[med-svn] [python-fitbit] 01/01: Imported Upstream version 0.2

Iain R. Learmonth irl at moszumanska.debian.org
Mon Apr 4 19:02:29 UTC 2016


This is an automated email from the git hooks/post-receive script.

irl pushed a commit to branch upstream
in repository python-fitbit.

commit 007e6aa971dd95b6b7f4e1f96295802a652be63c
Author: Iain R. Learmonth <irl at debian.org>
Date:   Mon Apr 4 19:08:04 2016 +0100

    Imported Upstream version 0.2
---
 .travis.yml                     |  11 +-
 CHANGELOG.rst                   |   6 +
 README.rst                      |   2 +-
 docs/conf.py                    |  16 +-
 docs/index.rst                  |  13 +-
 fitbit/__init__.py              |   5 +-
 fitbit/api.py                   | 813 ++++++++++++++++++++--------------------
 fitbit_tests/__init__.py        |  23 +-
 fitbit_tests/test_api.py        | 497 +++++++++++++++++-------
 fitbit_tests/test_auth.py       | 140 ++++---
 fitbit_tests/test_exceptions.py |  14 +-
 gather_keys_cli.py              |  83 ----
 gather_keys_oauth2.py           |  81 ++++
 requirements/base.txt           |   4 +-
 requirements/dev.txt            |   4 +-
 requirements/test.txt           |   5 +-
 setup.py                        |   7 +-
 tox.ini                         |  13 +-
 18 files changed, 1031 insertions(+), 706 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index f1c6347..9c50862 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,14 @@
 language: python
-python: 3.3
+python: 3.5
 env:
-  - TOX_ENV=pypy
+  # Avoid testing pypy on travis until the following issue is fixed:
+  #   https://github.com/travis-ci/travis-ci/issues/4756
+  #- TOX_ENV=pypy
+  - TOX_ENV=py35
+  - TOX_ENV=py34
   - TOX_ENV=py33
-  - TOX_ENV=py32
   - TOX_ENV=py27
-  - TOX_ENV=py26
+  - TOX_ENV=docs
 install:
   - pip install coveralls tox
 script: tox -e  $TOX_ENV
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9724eb7..49b5c70 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,9 @@
+0.2 (2016-03-23)
+================
+
+* Drop OAuth1 support. See `OAuth1 deprecated <https://dev.fitbit.com/docs/oauth2/#oauth-1-0a-deprecated>`_
+* Drop py26 and py32 support
+
 0.1.3 (2015-02-04)
 ==================
 
diff --git a/README.rst b/README.rst
index ff23090..b57101d 100644
--- a/README.rst
+++ b/README.rst
@@ -18,7 +18,7 @@ For documentation: `http://python-fitbit.readthedocs.org/ <http://python-fitbit.
 Requirements
 ============
 
-* Python 2.6+
+* Python 2.7+
 * `python-dateutil`_ (always)
 * `requests-oauthlib`_ (always)
 * `Sphinx`_ (to create the documention)
diff --git a/docs/conf.py b/docs/conf.py
index 205c641..e1715a4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,7 +25,10 @@ sys.path.insert(0, os.path.abspath('..'))
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.viewcode'
+]
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -40,17 +43,18 @@ source_suffix = '.rst'
 master_doc = 'index'
 
 # General information about the project.
+import fitbit
 project = u'Python-Fitbit'
-copyright = u'Copyright 2012-2015 ORCAS'
+copyright = fitbit.__copyright__
 
 # 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.1.3'
+version = fitbit.__version__
 # The full version, including alpha/beta/rc tags.
-release = '0.1.3'
+release = fitbit.__release__
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -91,7 +95,7 @@ pygments_style = 'sphinx'
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
+html_theme = 'alabaster'
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
@@ -120,7 +124,7 @@ html_theme = 'default'
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = []
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
diff --git a/docs/index.rst b/docs/index.rst
index a237953..d773a73 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,15 +14,19 @@ measurements
 Quickstart
 ==========
 
-Here is some example usage::
+If you are only retrieving data that doesn't require authorization, then you can use the unauthorized interface::
 
     import fitbit
     unauth_client = fitbit.Fitbit('<consumer_key>', '<consumer_secret>')
     # certain methods do not require user keys
-    unauth_client.activities()
+    unauth_client.food_units()
 
-    # 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>', resource_owner_key='<user_key>', resource_owner_secret='<user_secret>')
+Here is an example of authorizing with OAuth 2.0::
+
+    # You'll have to gather the tokens on your own, or use
+    # ./gather_keys_oauth2.py
+    authd_client = fitbit.Fitbit('<consumer_key>', '<consumer_secret>',
+                                 access_token='<access_token>', refresh_token='<refresh_token>')
     authd_client.sleep()
 
 Fitbit API
@@ -45,4 +49,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/fitbit/__init__.py b/fitbit/__init__.py
index 9d37ed0..aa8cf36 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, FitbitOauthClient
+from .api import Fitbit, FitbitOauth2Client
 
 # Meta.
 
@@ -17,7 +17,8 @@ __author_email__ = 'bpitcher at orcasinc.com'
 __copyright__ = 'Copyright 2012-2015 ORCAS'
 __license__ = 'Apache 2.0'
 
-__version__ = '0.1.3'
+__version__ = '0.2'
+__release__ = '0.2'
 
 # Module namespace.
 
diff --git a/fitbit/api.py b/fitbit/api.py
index 06a83d4..df612e3 100644
--- a/fitbit/api.py
+++ b/fitbit/api.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
-import requests
-import json
+import base64
 import datetime
+import json
+import requests
 
 try:
     from urllib.parse import urlencode
@@ -9,8 +10,8 @@ except ImportError:
     # Python 2.x
     from urllib import urlencode
 
-from requests_oauthlib import OAuth1, OAuth1Session
-
+from requests_oauthlib import OAuth2, OAuth2Session
+from oauthlib.oauth2 import TokenExpiredError
 from fitbit.exceptions import (BadResponse, DeleteError, HTTPBadRequest,
                                HTTPUnauthorized, HTTPForbidden,
                                HTTPServerError, HTTPConflict, HTTPNotFound,
@@ -18,45 +19,36 @@ from fitbit.exceptions import (BadResponse, DeleteError, HTTPBadRequest,
 from fitbit.utils import curry
 
 
-class FitbitOauthClient(object):
+class FitbitOauth2Client(object):
     API_ENDPOINT = "https://api.fitbit.com"
     AUTHORIZE_ENDPOINT = "https://www.fitbit.com"
     API_VERSION = 1
 
-    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
+    request_token_url = "%s/oauth2/token" % API_ENDPOINT
+    authorization_url = "%s/oauth2/authorize" % AUTHORIZE_ENDPOINT
+    access_token_url = request_token_url
+    refresh_token_url = request_token_url
 
-    def __init__(self, client_key, client_secret, resource_owner_key=None,
-                 resource_owner_secret=None, user_id=None, callback_uri=None,
+    def __init__(self, client_id, client_secret,
+                 access_token=None, refresh_token=None,
                  *args, **kwargs):
         """
-        Create a FitbitOauthClient object. Specify the first 5 parameters if
+        Create a FitbitOauth2Client object. Specify the first 7 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.
+        to start the setup for user authorization (as an example see gather_key_oauth2.py)
+            - client_id, client_secret are in the app configuration page
+            https://dev.fitbit.com/apps
+            - access_token, refresh_token are obtained after the user grants permission
         """
 
         self.session = requests.Session()
-        self.client_key = client_key
+        self.client_id = client_id
         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
-        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)
+        self.token = {
+            'access_token': access_token,
+            'refresh_token': refresh_token
+        }
+        self.oauth = OAuth2Session(client_id)
 
     def _request(self, method, url, **kwargs):
         """
@@ -66,16 +58,35 @@ class FitbitOauthClient(object):
 
     def make_request(self, url, data={}, method=None, **kwargs):
         """
-        Builds and makes the OAuth Request, catches errors
+        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'
-        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)
+
+        try:
+            auth = OAuth2(client_id=self.client_id, token=self.token)
+            response = self._request(method, url, data=data, auth=auth, **kwargs)
+        except 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'] == 'oauth' and
+                    d['errors'][0]['fieldName'] == 'access_token' and
+                    d['errors'][0]['message'].find('Access token invalid or 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)
@@ -96,50 +107,69 @@ class FitbitOauthClient(object):
             raise HTTPBadRequest(response)
         return response
 
-    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 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, **kwargs):
-        """Step 2: Return the URL the user needs to go to in order to grant us
+    def authorize_token_url(self, scope=None, redirect_uri=None, **kwargs):
+        """Step 1: 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.  Allow the client to request the mobile display by passing
-        the display='touch' argument.
-        """
+        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"
+            ]
+
+        if redirect_uri:
+            self.oauth.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.oauth.authorization_url(self.authorization_url, **kwargs)
+    def fetch_access_token(self, code, redirect_uri):
 
-    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
+        """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
         """
-        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 = self.oauth.fetch_access_token(self.access_token_url)
-
-        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
+        auth = OAuth2Session(self.client_id, redirect_uri=redirect_uri)
+        self.token = auth.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
+        """
+
+        unenc_str = (self.client_id + ':' + self.client_secret).encode('utf8')
+        headers = {
+            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+            'Authorization': b'Basic ' + base64.b64encode(unenc_str)
+        }
+        self.token = self.oauth.refresh_token(
+            self.refresh_token_url,
+            refresh_token=self.token['refresh_token'],
+            headers=headers)
+        return self.token
 
 
 class Fitbit(object):
@@ -149,50 +179,55 @@ class Fitbit(object):
     API_ENDPOINT = "https://api.fitbit.com"
     API_VERSION = 1
     WEEK_DAYS = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']
+    PERIODS = ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max']
 
-    _resource_list = [
+    RESOURCE_LIST = [
         'body',
         'activities',
         'foods/log',
-        'water',
+        'foods/log/water',
         'sleep',
         'heart',
         'bp',
         'glucose',
     ]
 
-    _qualifiers = [
+    QUALIFIERS = [
         'recent',
         'favorite',
         'frequent',
     ]
 
-    def __init__(self, client_key, client_secret, system=US, **kwargs):
-        self.client = FitbitOauthClient(client_key, client_secret, **kwargs)
-        self.SYSTEM = system
+    def __init__(self, client_id, client_secret, system=US, **kwargs):
+        """
+        Fitbit(<id>, <secret>, access_token=<token>, refresh_token=<token>)
+        """
+        self.system = system
+        self.client = FitbitOauth2Client(client_id, client_secret, **kwargs)
 
         # All of these use the same patterns, define the method for accessing
         # creating and deleting records once, and use curry to make individual
         # Methods for each
-        for resource in self._resource_list:
-            setattr(self, resource.replace('/', '_'),
+        for resource in Fitbit.RESOURCE_LIST:
+            underscore_resource = resource.replace('/', '_')
+            setattr(self, underscore_resource,
                     curry(self._COLLECTION_RESOURCE, resource))
 
             if resource not in ['body', 'glucose']:
                 # Body and Glucose entries are not currently able to be deleted
-                setattr(self, 'delete_%s' % resource, curry(
+                setattr(self, 'delete_%s' % underscore_resource, curry(
                     self._DELETE_COLLECTION_RESOURCE, resource))
 
-        for qualifier in self._qualifiers:
+        for qualifier in Fitbit.QUALIFIERS:
             setattr(self, '%s_activities' % qualifier, curry(self.activity_stats, qualifier=qualifier))
             setattr(self, '%s_foods' % qualifier, curry(self._food_stats,
                                                         qualifier=qualifier))
 
     def make_request(self, *args, **kwargs):
-        ##@ This should handle data level errors, improper requests, and bad
+        # This should handle data level errors, improper requests, and bad
         # serialization
         headers = kwargs.get('headers', {})
-        headers.update({'Accept-Language': self.SYSTEM})
+        headers.update({'Accept-Language': self.system})
         kwargs['headers'] = headers
 
         method = kwargs.get('method', 'POST' if 'data' in kwargs else 'GET')
@@ -224,10 +259,7 @@ class Fitbit(object):
 
         https://wiki.fitbit.com/display/API/API-Get-User-Info
         """
-        if user_id is None:
-            user_id = "-"
-        url = "%s/%s/user/%s/profile.json" % (self.API_ENDPOINT,
-                                              self.API_VERSION, user_id)
+        url = "{0}/{1}/user/{2}/profile.json".format(*self._get_common_args(user_id))
         return self.make_request(url)
 
     def user_profile_update(self, data):
@@ -241,10 +273,21 @@ class Fitbit(object):
 
         https://wiki.fitbit.com/display/API/API-Update-User-Info
         """
-        url = "%s/%s/user/-/profile.json" % (self.API_ENDPOINT,
-                                              self.API_VERSION)
+        url = "{0}/{1}/user/-/profile.json".format(*self._get_common_args())
         return self.make_request(url, data)
 
+    def _get_common_args(self, user_id=None):
+        common_args = (self.API_ENDPOINT, self.API_VERSION,)
+        if not user_id:
+            user_id = '-'
+        common_args += (user_id,)
+        return common_args
+
+    def _get_date_string(self, date):
+        if not isinstance(date, str):
+            return date.strftime('%Y-%m-%d')
+        return date
+
     def _COLLECTION_RESOURCE(self, resource, date=None, user_id=None,
                              data=None):
         """
@@ -261,7 +304,7 @@ class Fitbit(object):
             body(date=None, user_id=None, data=None)
             activities(date=None, user_id=None, data=None)
             foods_log(date=None, user_id=None, data=None)
-            water(date=None, user_id=None, data=None)
+            foods_log_water(date=None, user_id=None, data=None)
             sleep(date=None, user_id=None, data=None)
             heart(date=None, user_id=None, data=None)
             bp(date=None, user_id=None, data=None)
@@ -271,27 +314,15 @@ class Fitbit(object):
 
         if not date:
             date = datetime.date.today()
-        if not user_id:
-            user_id = '-'
-        if not isinstance(date, str):
-            date = date.strftime('%Y-%m-%d')
+        date_string = self._get_date_string(date)
 
+        kwargs = {'resource': resource, 'date': date_string}
         if not data:
-            url = "%s/%s/user/%s/%s/date/%s.json" % (
-                self.API_ENDPOINT,
-                self.API_VERSION,
-                user_id,
-                resource,
-                date,
-            )
+            base_url = "{0}/{1}/user/{2}/{resource}/date/{date}.json"
         else:
-            data['date'] = date
-            url = "%s/%s/user/%s/%s.json" % (
-                self.API_ENDPOINT,
-                self.API_VERSION,
-                user_id,
-                resource,
-            )
+            data['date'] = date_string
+            base_url = "{0}/{1}/user/{2}/{resource}.json"
+        url = base_url.format(*self._get_common_args(user_id), **kwargs)
         return self.make_request(url, data)
 
     def _DELETE_COLLECTION_RESOURCE(self, resource, log_id):
@@ -306,22 +337,159 @@ class Fitbit(object):
 
             delete_body(log_id)
             delete_activities(log_id)
-            delete_foods(log_id)
-            delete_water(log_id)
+            delete_foods_log(log_id)
+            delete_foods_log_water(log_id)
             delete_sleep(log_id)
             delete_heart(log_id)
             delete_bp(log_id)
 
         """
-        url = "%s/%s/user/-/%s/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            resource,
-            log_id,
+        url = "{0}/{1}/user/-/{resource}/{log_id}.json".format(
+            *self._get_common_args(),
+            resource=resource,
+            log_id=log_id
         )
         response = self.make_request(url, method='DELETE')
         return response
 
+    def _resource_goal(self, resource, data={}, period=None):
+        """ Handles GETting and POSTing resource goals of all types """
+        url = "{0}/{1}/user/-/{resource}/goal{postfix}.json".format(
+            *self._get_common_args(),
+            resource=resource,
+            postfix=('s/' + period) if period else ''
+        )
+        return self.make_request(url, data=data)
+
+    def _filter_nones(self, data):
+        filter_nones = lambda item: item[1] is not None
+        filtered_kwargs = list(filter(filter_nones, data.items()))
+        return {} if not filtered_kwargs else dict(filtered_kwargs)
+
+    def body_fat_goal(self, fat=None):
+        """
+        Implements the following APIs
+
+        * https://wiki.fitbit.com/display/API/API-Get-Body-Fat
+        * https://wiki.fitbit.com/display/API/API-Update-Fat-Goal
+
+        Pass no arguments to get the body fat goal. Pass a ``fat`` argument
+        to update the body fat goal.
+
+        Arguments:
+        * ``fat`` -- Target body fat in %; in the format X.XX
+        """
+        return self._resource_goal('body/log/fat', {'fat': fat} if fat else {})
+
+    def body_weight_goal(self, start_date=None, start_weight=None, weight=None):
+        """
+        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
+
+        Pass no arguments to get the body weight goal. Pass ``start_date``,
+        ``start_weight`` and optionally ``weight`` to set the weight goal.
+        ``weight`` is required if it hasn't been set yet.
+
+        Arguments:
+        * ``start_date`` -- Weight goal start date; in the format yyyy-MM-dd
+        * ``start_weight`` -- Weight goal start weight; in the format X.XX
+        * ``weight`` -- Weight goal target weight; in the format X.XX
+        """
+        data = self._filter_nones({
+            'startDate': start_date,
+            'startWeight': start_weight,
+            'weight': weight
+        })
+        if data and not ('startDate' in data and 'startWeight' in data):
+            raise ValueError('start_date and start_weight are both required')
+        return self._resource_goal('body/log/weight', data)
+
+    def activities_daily_goal(self, calories_out=None, active_minutes=None,
+                              floors=None, distance=None, steps=None):
+        """
+        Implements the following APIs
+
+        https://wiki.fitbit.com/display/API/API-Get-Activity-Daily-Goals
+        https://wiki.fitbit.com/display/API/API-Update-Activity-Daily-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
+        goal.
+
+        Arguments:
+        * ``calories_out`` -- New goal value; in an integer format
+        * ``active_minutes`` -- New goal value; in an integer format
+        * ``floors`` -- New goal value; in an integer format
+        * ``distance`` -- New goal value; in the format X.XX or integer
+        * ``steps`` -- New goal value; in an integer format
+        """
+        data = self._filter_nones({
+            'caloriesOut': calories_out,
+            'activeMinutes': active_minutes,
+            'floors': floors,
+            'distance': distance,
+            'steps': steps
+        })
+        return self._resource_goal('activities', data, period='daily')
+
+    def activities_weekly_goal(self, distance=None, floors=None, steps=None):
+        """
+        Implements the following APIs
+
+        https://wiki.fitbit.com/display/API/API-Get-Activity-Weekly-Goals
+        https://wiki.fitbit.com/display/API/API-Update-Activity-Weekly-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
+        goal.
+
+        Arguments:
+        * ``distance`` -- New goal value; in the format X.XX or integer
+        * ``floors`` -- New goal value; in an integer format
+        * ``steps`` -- New goal value; in an integer format
+        """
+        data = self._filter_nones({'distance': distance, 'floors': floors,
+                                   'steps': steps})
+        return self._resource_goal('activities', data, period='weekly')
+
+    def food_goal(self, calories=None, intensity=None, personalized=None):
+        """
+        Implements the following APIs
+
+        https://wiki.fitbit.com/display/API/API-Get-Food-Goals
+        https://wiki.fitbit.com/display/API/API-Update-Food-Goals
+
+        Pass no arguments to get the food goal. Pass at least ``calories`` or
+        ``intensity`` and optionally ``personalized`` to update the food goal.
+
+        Arguments:
+        * ``calories`` -- Manual Calorie Consumption Goal; calories, integer;
+        * ``intensity`` -- Food Plan intensity; (MAINTENANCE, EASIER, MEDIUM, KINDAHARD, HARDER);
+        * ``personalized`` -- Food Plan type; ``True`` or ``False``
+        """
+        data = self._filter_nones({'calories': calories, 'intensity': intensity,
+                                   'personalized': personalized})
+        if data and not ('calories' in data or 'intensity' in data):
+            raise ValueError('Either calories or intensity is required')
+        return self._resource_goal('foods/log', data)
+
+    def water_goal(self, target=None):
+        """
+        Implements the following APIs
+
+        https://wiki.fitbit.com/display/API/API-Get-Water-Goal
+        https://wiki.fitbit.com/display/API/API-Update-Water-Goal
+
+        Pass no arguments to get the water goal. Pass ``target`` to update it.
+
+        Arguments:
+        * ``target`` -- Target water goal in the format X.X, will be set in unit based on locale
+        """
+        data = self._filter_nones({'target': target})
+        return self._resource_goal('foods/log/water', data)
+
     def time_series(self, resource, user_id=None, base_date='today',
                     period=None, end_date=None):
         """
@@ -334,32 +502,22 @@ class Fitbit(object):
 
         https://wiki.fitbit.com/display/API/API-Get-Time-Series
         """
-        if not user_id:
-            user_id = '-'
-
         if period and end_date:
             raise TypeError("Either end_date or period can be specified, not both")
 
         if end_date:
-            if not isinstance(end_date, str):
-                end = end_date.strftime('%Y-%m-%d')
-            else:
-                end = end_date
+            end = self._get_date_string(end_date)
         else:
-            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'")
+            if not period in Fitbit.PERIODS:
+                raise ValueError("Period must be one of %s"
+                                 % ','.join(Fitbit.PERIODS))
             end = period
 
-        if not isinstance(base_date, str):
-            base_date = base_date.strftime('%Y-%m-%d')
-
-        url = "%s/%s/user/%s/%s/date/%s/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            user_id,
-            resource,
-            base_date,
-            end
+        url = "{0}/{1}/user/{2}/{resource}/date/{base_date}/{end}.json".format(
+            *self._get_common_args(user_id),
+            resource=resource,
+            base_date=self._get_date_string(base_date),
+            end=end
         )
         return self.make_request(url)
 
@@ -373,37 +531,35 @@ class Fitbit(object):
         https://wiki.fitbit.com/display/API/API-Get-Intraday-Time-Series
         """
 
-        if start_time and not end_time:
-            raise TypeError("You must provide an end time when you provide a start time")
-
-        if end_time and not start_time:
-            raise TypeError("You must provide a start time when you provide an end time")
+        # Check that the time range is valid
+        time_test = lambda t: not (t is None or isinstance(t, str) and not t)
+        time_map = list(map(time_test, [start_time, end_time]))
+        if not all(time_map) and any(time_map):
+            raise TypeError('You must provide both the end and start time or neither')
 
-        if not isinstance(base_date, str):
-            base_date = base_date.strftime('%Y-%m-%d')
-
-        if not detail_level in ['1min', '15min']:
-                raise ValueError("Period must be either '1min' or '15min'")
+        """
+        Per
+        https://wiki.fitbit.com/display/API/API-Get-Intraday-Time-Series
+        the detail-level is now (OAuth 2.0 ):
+        either "1min" or "15min" (optional). "1sec" for heart rate.
+        """
+        if not detail_level in ['1sec', '1min', '15min']:
+            raise ValueError("Period must be either '1sec', '1min', or '15min'")
 
-        url = "%s/%s/user/-/%s/date/%s/1d/%s" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            resource,
-            base_date,
-            detail_level
+        url = "{0}/{1}/user/-/{resource}/date/{base_date}/1d/{detail_level}".format(
+            *self._get_common_args(),
+            resource=resource,
+            base_date=self._get_date_string(base_date),
+            detail_level=detail_level
         )
 
-        if start_time:
-            time_init = start_time
-            if not isinstance(time_init, str):
-                time_init = start_time.strftime('%H:%M')
-            url = url + ('/time/%s' % (time_init))
-
-        if end_time:
-            time_fin = end_time
-            if not isinstance(time_fin, str):
-                time_fin = time_fin.strftime('%H:%M')
-            url = url + ('/%s' % (time_fin))
+        if all(time_map):
+            url = url + '/time'
+            for time in [start_time, end_time]:
+                time_str = time
+                if not isinstance(time_str, str):
+                    time_str = time.strftime('%H:%M')
+                url = url + ('/%s' % (time_str))
 
         url = url + '.json'
 
@@ -422,23 +578,18 @@ class Fitbit(object):
             favorite_activities(user_id=None, qualifier='')
             frequent_activities(user_id=None, qualifier='')
         """
-        if not user_id:
-            user_id = '-'
-
         if qualifier:
-            if qualifier in self._qualifiers:
+            if qualifier in Fitbit.QUALIFIERS:
                 qualifier = '/%s' % qualifier
             else:
                 raise ValueError("Qualifier must be one of %s"
-                    % ', '.join(self._qualifiers))
+                                 % ', '.join(Fitbit.QUALIFIERS))
         else:
             qualifier = ''
 
-        url = "%s/%s/user/%s/activities%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            user_id,
-            qualifier,
+        url = "{0}/{1}/user/{2}/activities{qualifier}.json".format(
+            *self._get_common_args(user_id),
+            qualifier=qualifier
         )
         return self.make_request(url)
 
@@ -454,14 +605,9 @@ class Fitbit(object):
         * https://wiki.fitbit.com/display/API/API-Get-Frequent-Foods
         * https://wiki.fitbit.com/display/API/API-Get-Favorite-Foods
         """
-        if not user_id:
-            user_id = '-'
-
-        url = "%s/%s/user/%s/foods/log/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            user_id,
-            qualifier,
+        url = "{0}/{1}/user/{2}/foods/log/{qualifier}.json".format(
+            *self._get_common_args(user_id),
+            qualifier=qualifier
         )
         return self.make_request(url)
 
@@ -469,10 +615,9 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Add-Favorite-Activity
         """
-        url = "%s/%s/user/-/activities/favorite/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            activity_id,
+        url = "{0}/{1}/user/-/activities/favorite/{activity_id}.json".format(
+            *self._get_common_args(),
+            activity_id=activity_id
         )
         return self.make_request(url, method='POST')
 
@@ -480,19 +625,16 @@ class Fitbit(object):
         """
         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)
+        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
         """
-        url = "%s/%s/user/-/activities/favorite/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            activity_id,
+        url = "{0}/{1}/user/-/activities/favorite/{activity_id}.json".format(
+            *self._get_common_args(),
+            activity_id=activity_id
         )
         return self.make_request(url, method='DELETE')
 
@@ -500,10 +642,9 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Add-Favorite-Food
         """
-        url = "%s/%s/user/-/foods/log/favorite/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            food_id,
+        url = "{0}/{1}/user/-/foods/log/favorite/{food_id}.json".format(
+            *self._get_common_args(),
+            food_id=food_id
         )
         return self.make_request(url, method='POST')
 
@@ -511,10 +652,9 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Delete-Favorite-Food
         """
-        url = "%s/%s/user/-/foods/log/favorite/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            food_id,
+        url = "{0}/{1}/user/-/foods/log/favorite/{food_id}.json".format(
+            *self._get_common_args(),
+            food_id=food_id
         )
         return self.make_request(url, method='DELETE')
 
@@ -522,53 +662,43 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Create-Food
         """
-        url = "%s/%s/user/-/foods.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-        )
+        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
         """
-        url = "%s/%s/user/-/meals.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-        )
+        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
         """
-        url = "%s/%s/user/-/devices.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-        )
+        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
         """
-        url = "%s/%s/user/-/devices/tracker/%s/alarms.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            device_id
+        url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms.json".format(
+            *self._get_common_args(),
+            device_id=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'):
+    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
+        url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms.json".format(
+            *self._get_common_args(),
+            device_id=device_id
         )
         alarm_time = alarm_time.strftime("%H:%M%z")
         # Check week_days list
@@ -606,11 +736,10 @@ class Fitbit(object):
         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
+        url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms/{alarm_id}.json".format(
+            *self._get_common_args(),
+            device_id=device_id,
+            alarm_id=alarm_id
         )
         alarm_time = alarm_time.strftime("%H:%M%z")
 
@@ -634,11 +763,10 @@ class Fitbit(object):
         """
         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
+        url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms/{alarm_id}.json".format(
+            *self._get_common_args(),
+            device_id=device_id,
+            alarm_id=alarm_id
         )
         return self.make_request(url, method="DELETE")
 
@@ -647,12 +775,11 @@ class Fitbit(object):
         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
+        url = "{0}/{1}/user/-/sleep/date/{year}-{month}-{day}.json".format(
+            *self._get_common_args(),
+            year=date.year,
+            month=date.month,
+            day=date.day
         )
         return self.make_request(url)
 
@@ -666,30 +793,23 @@ class Fitbit(object):
             'duration': duration,
             'date': start_time.strftime("%Y-%m-%d"),
         }
-        url = "%s/%s/user/-/sleep.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-        )
+        url = "{0}/{1}/user/-/sleep.json".format(*self._get_common_args())
         return self.make_request(url, data=data, method="POST")
 
     def activities_list(self):
         """
         https://wiki.fitbit.com/display/API/API-Browse-Activities
         """
-        url = "%s/%s/activities.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-        )
+        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
         """
-        url = "%s/%s/activities/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            activity_id
+        url = "{0}/{1}/activities/{activity_id}.json".format(
+            *self._get_common_args(),
+            activity_id=activity_id
         )
         return self.make_request(url)
 
@@ -697,10 +817,9 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Search-Foods
         """
-        url = "%s/%s/foods/search.json?%s" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            urlencode({'query': query})
+        url = "{0}/{1}/foods/search.json?{encoded_query}".format(
+            *self._get_common_args(),
+            encoded_query=urlencode({'query': query})
         )
         return self.make_request(url)
 
@@ -708,10 +827,9 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Get-Food
         """
-        url = "%s/%s/foods/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            food_id
+        url = "{0}/{1}/foods/{food_id}.json".format(
+            *self._get_common_args(),
+            food_id=food_id
         )
         return self.make_request(url)
 
@@ -719,10 +837,7 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Get-Food-Units
         """
-        url = "%s/%s/foods/units.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION
-        )
+        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):
@@ -734,52 +849,7 @@ class Fitbit(object):
 
         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)
+        return self._get_body('weight', base_date, user_id, period, end_date)
 
     def get_bodyfat(self, base_date=None, user_id=None, period=None, end_date=None):
         """
@@ -790,64 +860,39 @@ class Fitbit(object):
 
         You can specify period or end_date, or neither, but not both.
         """
+        return self._get_body('fat', base_date, user_id, period, end_date)
+
+    def _get_body(self, type_, base_date=None, user_id=None, period=None,
+                  end_date=None):
         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
+        base_date_string = self._get_date_string(base_date)
 
+        kwargs = {'type_': type_}
+        base_url = "{0}/{1}/user/{2}/body/log/{type_}/date/{date_string}.json"
         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
-            )
+            if not period in Fitbit.PERIODS:
+                raise ValueError("Period must be one of %s" %
+                                 ','.join(Fitbit.PERIODS))
+            kwargs['date_string'] = '/'.join([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
-            )
+            end_string = self._get_date_string(end_date)
+            kwargs['date_string'] = '/'.join([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,
-            )
+            kwargs['date_string'] = base_date_string
+
+        url = base_url.format(*self._get_common_args(user_id), **kwargs)
         return self.make_request(url)
 
     def get_friends(self, user_id=None):
         """
         https://wiki.fitbit.com/display/API/API-Get-Friends
         """
-        if not user_id:
-            user_id = '-'
-        url = "%s/%s/user/%s/friends.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            user_id
-        )
+        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):
@@ -856,10 +901,9 @@ class Fitbit(object):
         """
         if not period in ['7d', '30d']:
             raise ValueError("Period must be one of '7d', '30d'")
-        url = "%s/%s/user/-/friends/leaders/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            period
+        url = "{0}/{1}/user/-/friends/leaders/{period}.json".format(
+            *self._get_common_args(),
+            period=period
         )
         return self.make_request(url)
 
@@ -867,10 +911,7 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Create-Invite
         """
-        url = "%s/%s/user/-/friends/invitations.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-        )
+        url = "{0}/{1}/user/-/friends/invitations.json".format(*self._get_common_args())
         return self.make_request(url, data=data)
 
     def invite_friend_by_email(self, email):
@@ -891,10 +932,9 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Accept-Invite
         """
-        url = "%s/%s/user/-/friends/invitations/%s.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            other_user_id,
+        url = "{0}/{1}/user/-/friends/invitations/{user_id}.json".format(
+            *self._get_common_args(),
+            user_id=other_user_id
         )
         accept = 'true' if accept else 'false'
         return self.make_request(url, data={'accept': accept})
@@ -915,13 +955,7 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/API-Get-Badges
         """
-        if not user_id:
-            user_id = '-'
-        url = "%s/%s/user/%s/badges.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            user_id
-        )
+        url = "{0}/{1}/user/{2}/badges.json".format(*self._get_common_args(user_id))
         return self.make_request(url)
 
     def subscription(self, subscription_id, subscriber_id, collection=None,
@@ -929,22 +963,15 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API
         """
-        if not collection:
-            url = "%s/%s/user/-/apiSubscriptions/%s.json" % (
-                self.API_ENDPOINT,
-                self.API_VERSION,
-                subscription_id
-            )
-        else:
-            url = "%s/%s/user/-/%s/apiSubscriptions/%s-%s.json" % (
-                self.API_ENDPOINT,
-                self.API_VERSION,
-                collection,
-                subscription_id,
-                collection
-            )
+        base_url = "{0}/{1}/user/-{collection}/apiSubscriptions/{end_string}.json"
+        kwargs = {'collection': '', 'end_string': subscription_id}
+        if collection:
+            kwargs = {
+                'end_string': '-'.join([subscription_id, collection]),
+                'collection': '/' + collection
+            }
         return self.make_request(
-            url,
+            base_url.format(*self._get_common_args(), **kwargs),
             method=method,
             headers={"X-Fitbit-Subscriber-id": subscriber_id}
         )
@@ -953,18 +980,8 @@ class Fitbit(object):
         """
         https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API
         """
-        if collection:
-            collection = '/%s' % collection
-        url = "%s/%s/user/-%s/apiSubscriptions.json" % (
-            self.API_ENDPOINT,
-            self.API_VERSION,
-            collection,
+        url = "{0}/{1}/user/-{collection}/apiSubscriptions.json".format(
+            *self._get_common_args(),
+            collection='/{0}'.format(collection) if collection else ''
         )
         return self.make_request(url)
-
-    @classmethod
-    def from_oauth_keys(self, client_key, client_secret, user_key=None,
-                        user_secret=None, user_id=None, system=US):
-        client = FitbitOauthClient(client_key, client_secret, user_key,
-                                   user_secret, user_id)
-        return self(client, system)
diff --git a/fitbit_tests/__init__.py b/fitbit_tests/__init__.py
index e3e0700..d5f28f7 100644
--- a/fitbit_tests/__init__.py
+++ b/fitbit_tests/__init__.py
@@ -1,21 +1,24 @@
 import unittest
 from .test_exceptions import ExceptionTest
-from .test_auth import AuthTest
-from .test_api import APITest, CollectionResourceTest, DeleteCollectionResourceTest, MiscTest
+from .test_auth import Auth2Test
+from .test_api import (
+    APITest,
+    CollectionResourceTest,
+    DeleteCollectionResourceTest,
+    ResourceAccessTest,
+    SubscriptionsTest,
+    PartnerAPITest
+)
 
 
 def all_tests(consumer_key="", consumer_secret="", user_key=None, user_secret=None):
-    kwargs = {
-        "consumer_key": consumer_key,
-        "consumer_secret": consumer_secret,
-        "user_key": user_key,
-        "user_secret": user_secret,
-    }
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(ExceptionTest))
-    suite.addTest(unittest.makeSuite(AuthTest))
+    suite.addTest(unittest.makeSuite(Auth2Test))
     suite.addTest(unittest.makeSuite(APITest))
     suite.addTest(unittest.makeSuite(CollectionResourceTest))
     suite.addTest(unittest.makeSuite(DeleteCollectionResourceTest))
-    suite.addTest(unittest.makeSuite(MiscTest))
+    suite.addTest(unittest.makeSuite(ResourceAccessTest))
+    suite.addTest(unittest.makeSuite(SubscriptionsTest))
+    suite.addTest(unittest.makeSuite(PartnerAPITest))
     return suite
diff --git a/fitbit_tests/test_api.py b/fitbit_tests/test_api.py
index bef4aa0..651a189 100644
--- a/fitbit_tests/test_api.py
+++ b/fitbit_tests/test_api.py
@@ -16,21 +16,25 @@ class TestBase(TestCase):
         # arguments and verify that make_request is called with the expected args and kwargs
         with mock.patch.object(self.fb, 'make_request') as make_request:
             retval = getattr(self.fb, funcname)(*args, **kwargs)
-        args, kwargs = make_request.call_args
-        self.assertEqual(expected_args, args)
-        self.assertEqual(expected_kwargs, kwargs)
+        mr_args, mr_kwargs = make_request.call_args
+        self.assertEqual(expected_args, mr_args)
+        self.assertEqual(expected_kwargs, mr_kwargs)
 
     def verify_raises(self, funcname, args, kwargs, exc):
         self.assertRaises(exc, getattr(self.fb, funcname), *args, **kwargs)
 
+
 class APITest(TestBase):
-    """Tests for python-fitbit API, not directly involved in getting authenticated"""
+    """
+    Tests for python-fitbit API, not directly involved in getting
+    authenticated
+    """
 
     def test_make_request(self):
         # If make_request returns a response with status 200,
         # we get back the json decoded value that was in the response.content
         ARGS = (1, 2)
-        KWARGS = { 'a': 3, 'b': 4, 'headers': {'Accept-Language': self.fb.SYSTEM}}
+        KWARGS = {'a': 3, 'b': 4, 'headers': {'Accept-Language': self.fb.system}}
         mock_response = mock.Mock()
         mock_response.status_code = 200
         mock_response.content = b"1"
@@ -50,7 +54,7 @@ class APITest(TestBase):
         mock_response.status_code = 202
         mock_response.content = "1"
         ARGS = (1, 2)
-        KWARGS = { 'a': 3, 'b': 4, 'Accept-Language': self.fb.SYSTEM}
+        KWARGS = {'a': 3, 'b': 4, 'Accept-Language': self.fb.system}
         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)
@@ -63,7 +67,7 @@ class APITest(TestBase):
         mock_response.status_code = 204
         mock_response.content = "1"
         ARGS = (1, 2)
-        KWARGS = { 'a': 3, 'b': 4, 'method': 'DELETE', 'Accept-Language': self.fb.SYSTEM}
+        KWARGS = {'a': 3, 'b': 4, 'method': 'DELETE', 'Accept-Language': self.fb.system}
         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)
@@ -76,29 +80,20 @@ class APITest(TestBase):
         mock_response.status_code = 205
         mock_response.content = "1"
         ARGS = (1, 2)
-        KWARGS = { 'a': 3, 'b': 4, 'method': 'DELETE', 'Accept-Language': self.fb.SYSTEM}
+        KWARGS = {'a': 3, 'b': 4, 'method': 'DELETE', 'Accept-Language': self.fb.system}
         with mock.patch.object(self.fb.client, 'make_request') as client_make_request:
             client_make_request.return_value = mock_response
             self.assertRaises(DeleteError, self.fb.make_request, *ARGS, **KWARGS)
 
-    def test_user_profile_get(self):
-        user_id = "FOO"
-        url = URLBASE + "/%s/profile.json" % user_id
-        self.common_api_test('user_profile_get', (user_id,), {}, (url,), {})
-
-    def test_user_profile_update(self):
-        data = "BAR"
-        url = URLBASE + "/-/profile.json"
-        self.common_api_test('user_profile_update', (data,), {}, (url, data), {})
 
 class CollectionResourceTest(TestBase):
-    """Tests for _COLLECTION_RESOURCE"""
+    """ Tests for _COLLECTION_RESOURCE """
     def test_all_args(self):
         # If we pass all the optional args, the right things happen
         resource = "RESOURCE"
         date = datetime.date(1962, 1, 13)
         user_id = "bilbo"
-        data = { 'a': 1, 'b': 2}
+        data = {'a': 1, 'b': 2}
         expected_data = data.copy()
         expected_data['date'] = date.strftime("%Y-%m-%d")
         url = URLBASE + "/%s/%s.json" % (user_id, resource)
@@ -109,17 +104,17 @@ class CollectionResourceTest(TestBase):
         resource = "RESOURCE"
         date = "1962-1-13"
         user_id = "bilbo"
-        data = { 'a': 1, 'b': 2}
+        data = {'a': 1, 'b': 2}
         expected_data = data.copy()
         expected_data['date'] = date
         url = URLBASE + "/%s/%s.json" % (user_id, resource)
-        self.common_api_test('_COLLECTION_RESOURCE',(resource, date, user_id, data), {}, (url, expected_data), {} )
+        self.common_api_test('_COLLECTION_RESOURCE', (resource, date, user_id, data), {}, (url, expected_data), {})
 
     def test_no_date(self):
         # If we omit the date, it uses today
         resource = "RESOURCE"
         user_id = "bilbo"
-        data = { 'a': 1, 'b': 2}
+        data = {'a': 1, 'b': 2}
         expected_data = data.copy()
         expected_data['date'] = datetime.date.today().strftime("%Y-%m-%d")  # expect today
         url = URLBASE + "/%s/%s.json" % (user_id, resource)
@@ -130,12 +125,17 @@ class CollectionResourceTest(TestBase):
         resource = "RESOURCE"
         date = datetime.date(1962, 1, 13)
         user_id = None
-        data = { 'a': 1, 'b': 2}
+        data = {'a': 1, 'b': 2}
         expected_data = data.copy()
         expected_data['date'] = date.strftime("%Y-%m-%d")
         expected_user_id = "-"
         url = URLBASE + "/%s/%s.json" % (expected_user_id, resource)
-        self.common_api_test('_COLLECTION_RESOURCE', (resource, date, user_id, data), {}, (url,expected_data), {})
+        self.common_api_test(
+            '_COLLECTION_RESOURCE',
+            (resource, date, user_id, data), {},
+            (url, expected_data),
+            {}
+        )
 
     def test_no_data(self):
         # If we omit the data arg, it does the right thing
@@ -144,7 +144,7 @@ class CollectionResourceTest(TestBase):
         user_id = "bilbo"
         data = None
         url = URLBASE + "/%s/%s/date/%s.json" % (user_id, resource, date)
-        self.common_api_test('_COLLECTION_RESOURCE', (resource,date,user_id,data), {}, (url,data), {})
+        self.common_api_test('_COLLECTION_RESOURCE', (resource, date, user_id, data), {}, (url, data), {})
 
     def test_body(self):
         # Test the first method defined in __init__ to see if it calls
@@ -162,37 +162,82 @@ class CollectionResourceTest(TestBase):
         self.assertEqual({'date': 1, 'user_id': 2, 'data': 3}, kwargs)
         self.assertEqual(999, retval)
 
+
 class DeleteCollectionResourceTest(TestBase):
     """Tests for _DELETE_COLLECTION_RESOURCE"""
     def test_impl(self):
         # _DELETE_COLLECTION_RESOURCE calls make_request with the right args
         resource = "RESOURCE"
         log_id = "Foo"
-        url = URLBASE + "/-/%s/%s.json" % (resource,log_id)
-        self.common_api_test('_DELETE_COLLECTION_RESOURCE', (resource, log_id), {},
-            (url,), {"method": "DELETE"})
+        url = URLBASE + "/-/%s/%s.json" % (resource, log_id)
+        self.common_api_test(
+            '_DELETE_COLLECTION_RESOURCE',
+            (resource, log_id), {},
+            (url,),
+            {"method": "DELETE"}
+        )
 
     def test_cant_delete_body(self):
         self.assertFalse(hasattr(self.fb, 'delete_body'))
 
-    def test_delete_water(self):
+    def test_delete_foods_log(self):
+        log_id = "fake_log_id"
+        # We need to mock _DELETE_COLLECTION_RESOURCE before we create the Fitbit object,
+        # 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('x', 'y')
+            retval = fb.delete_foods_log(log_id=log_id)
+        args, kwargs = delete_resource.call_args
+        self.assertEqual(('foods/log',), args)
+        self.assertEqual({'log_id': log_id}, kwargs)
+        self.assertEqual(999, retval)
+
+    def test_delete_foods_log_water(self):
         log_id = "OmarKhayyam"
         # We need to mock _DELETE_COLLECTION_RESOURCE before we create the Fitbit object,
         # 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('x', 'y')
-            retval = fb.delete_water(log_id=log_id)
+            retval = fb.delete_foods_log_water(log_id=log_id)
         args, kwargs = delete_resource.call_args
-        self.assertEqual(('water',), args)
+        self.assertEqual(('foods/log/water',), args)
         self.assertEqual({'log_id': log_id}, kwargs)
         self.assertEqual(999, retval)
 
-class MiscTest(TestBase):
-    def test_activities(self):
-        user_id = "Qui-Gon Jinn"
-        self.common_api_test('activities', (), {}, (URLBASE + "/%s/activities/date/%s.json" % (user_id, datetime.date.today().strftime('%Y-%m-%d'),),), {})
-        self.common_api_test('activities', (), {}, (URLBASE + "/-/activities/date/%s.json" % datetime.date.today().strftime('%Y-%m-%d'),), {})
+
+class ResourceAccessTest(TestBase):
+    """
+    Class for testing the Fitbit Resource Access API:
+    https://wiki.fitbit.com/display/API/Fitbit+Resource+Access+API
+    """
+    def test_user_profile_get(self):
+        """
+        Test getting a user profile.
+        https://wiki.fitbit.com/display/API/API-Get-User-Info
+
+        Tests the following HTTP method/URLs:
+        GET https://api.fitbit.com/1/user/FOO/profile.json
+        GET https://api.fitbit.com/1/user/-/profile.json
+        """
+        user_id = "FOO"
+        url = URLBASE + "/%s/profile.json" % user_id
+        self.common_api_test('user_profile_get', (user_id,), {}, (url,), {})
+        url = URLBASE + "/-/profile.json"
+        self.common_api_test('user_profile_get', (), {}, (url,), {})
+
+    def test_user_profile_update(self):
+        """
+        Test updating a user profile.
+        https://wiki.fitbit.com/display/API/API-Update-User-Info
+
+        Tests the following HTTP method/URLs:
+        POST https://api.fitbit.com/1/user/-/profile.json
+        """
+        data = "BAR"
+        url = URLBASE + "/-/profile.json"
+        self.common_api_test('user_profile_update', (data,), {}, (url, data), {})
 
     def test_recent_activities(self):
         user_id = "LukeSkywalker"
@@ -214,6 +259,72 @@ class MiscTest(TestBase):
         qualifier = None
         self.common_api_test('activity_stats', (), dict(user_id=user_id, qualifier=qualifier), (URLBASE + "/%s/activities.json" % user_id,), {})
 
+    def test_body_fat_goal(self):
+        self.common_api_test(
+            'body_fat_goal', (), dict(),
+            (URLBASE + '/-/body/log/fat/goal.json',), {'data': {}})
+        self.common_api_test(
+            'body_fat_goal', (), dict(fat=10),
+            (URLBASE + '/-/body/log/fat/goal.json',), {'data': {'fat': 10}})
+
+    def test_body_weight_goal(self):
+        self.common_api_test(
+            'body_weight_goal', (), dict(),
+            (URLBASE + '/-/body/log/weight/goal.json',), {'data': {}})
+        self.common_api_test(
+            'body_weight_goal', (), dict(start_date='2015-04-01', start_weight=180),
+            (URLBASE + '/-/body/log/weight/goal.json',),
+            {'data': {'startDate': '2015-04-01', 'startWeight': 180}})
+        self.verify_raises('body_weight_goal', (), {'start_date': '2015-04-01'}, ValueError)
+        self.verify_raises('body_weight_goal', (), {'start_weight': 180}, ValueError)
+
+    def test_activities_daily_goal(self):
+        self.common_api_test(
+            'activities_daily_goal', (), dict(),
+            (URLBASE + '/-/activities/goals/daily.json',), {'data': {}})
+        self.common_api_test(
+            'activities_daily_goal', (), dict(steps=10000),
+            (URLBASE + '/-/activities/goals/daily.json',), {'data': {'steps': 10000}})
+        self.common_api_test(
+            'activities_daily_goal', (),
+            dict(calories_out=3107, active_minutes=30, floors=10, distance=5, steps=10000),
+            (URLBASE + '/-/activities/goals/daily.json',),
+            {'data': {'caloriesOut': 3107, 'activeMinutes': 30, 'floors': 10, 'distance': 5, 'steps': 10000}})
+
+    def test_activities_weekly_goal(self):
+        self.common_api_test(
+            'activities_weekly_goal', (), dict(),
+            (URLBASE + '/-/activities/goals/weekly.json',), {'data': {}})
+        self.common_api_test(
+            'activities_weekly_goal', (), dict(steps=10000),
+            (URLBASE + '/-/activities/goals/weekly.json',), {'data': {'steps': 10000}})
+        self.common_api_test(
+            'activities_weekly_goal', (),
+            dict(floors=10, distance=5, steps=10000),
+            (URLBASE + '/-/activities/goals/weekly.json',),
+            {'data': {'floors': 10, 'distance': 5, 'steps': 10000}})
+
+    def test_food_goal(self):
+        self.common_api_test(
+            'food_goal', (), dict(),
+            (URLBASE + '/-/foods/log/goal.json',), {'data': {}})
+        self.common_api_test(
+            'food_goal', (), dict(calories=2300),
+            (URLBASE + '/-/foods/log/goal.json',), {'data': {'calories': 2300}})
+        self.common_api_test(
+            'food_goal', (), dict(intensity='EASIER', personalized=True),
+            (URLBASE + '/-/foods/log/goal.json',),
+            {'data': {'intensity': 'EASIER', 'personalized': True}})
+        self.verify_raises('food_goal', (), {'personalized': True}, ValueError)
+
+    def test_water_goal(self):
+        self.common_api_test(
+            'water_goal', (), dict(),
+            (URLBASE + '/-/foods/log/water/goal.json',), {'data': {}})
+        self.common_api_test(
+            'water_goal', (), dict(target=63),
+            (URLBASE + '/-/foods/log/water/goal.json',), {'data': {'target': 63}})
+
     def test_timeseries(self):
         resource = 'FOO'
         user_id = 'BAR'
@@ -257,63 +368,10 @@ class MiscTest(TestBase):
         test_timeseries(self.fb, resource, user_id=user_id, base_date=datetime.date(1992,5,12), period=None, end_date=end_date,
             expected_url=URLBASE + "/BAR/FOO/date/1992-05-12/1998-12-31.json")
 
-    def test_intraday_timeseries(self):
-        resource = 'FOO'
-        base_date = '1918-05-11'
-
-        # detail_level must be valid
-        self.assertRaises(
-            ValueError,
-            self.fb.intraday_time_series,
-            resource,
-            base_date,
-            detail_level="xyz",
-            start_time=None,
-            end_time=None)
-
-        # provide end_time if start_time provided
-        self.assertRaises(
-            TypeError,
-            self.fb.intraday_time_series,
-            resource,
-            base_date,
-            detail_level="1min",
-            start_time='12:55',
-            end_time=None)
-
-        # provide start_time if end_time provided
-        self.assertRaises(
-            TypeError,
-            self.fb.intraday_time_series,
-            resource,
-            base_date,
-            detail_level="1min",
-            start_time=None,
-            end_time='12:55')
-
-        def test_intraday_timeseries(fb, resource, base_date, detail_level, start_time, end_time, expected_url):
-            with mock.patch.object(fb, 'make_request') as make_request:
-                retval = fb.intraday_time_series(resource, base_date, detail_level, start_time, end_time)
-            args, kwargs = make_request.call_args
-            self.assertEqual((expected_url,), args)
-
-        # Default
-        test_intraday_timeseries(self.fb, resource, base_date=base_date,
-                                 detail_level='1min', start_time=None, end_time=None,
-                                 expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min.json")
-        # start_date can be a date object
-        test_intraday_timeseries(self.fb, resource, base_date=datetime.date(1918, 5, 11),
-                                 detail_level='1min', start_time=None, end_time=None,
-                                 expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min.json")
-        # start_time can be a datetime object
-        test_intraday_timeseries(self.fb, resource, base_date=base_date,
-                                 detail_level='1min', start_time=datetime.time(3,56), end_time='15:07',
-                                 expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/03:56/15:07.json")
-        # end_time can be a datetime object
-        test_intraday_timeseries(self.fb, resource, base_date=base_date,
-                                 detail_level='1min', start_time='3:56', end_time=datetime.time(15,7),
-                                 expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/3:56/15:07.json")
-
+    def test_sleep(self):
+        today = datetime.date.today().strftime('%Y-%m-%d')
+        self.common_api_test('sleep', (today,), {}, ("%s/-/sleep/date/%s.json" % (URLBASE, today), None), {})
+        self.common_api_test('sleep', (today, "USER_ID"), {}, ("%s/USER_ID/sleep/date/%s.json" % (URLBASE, today), None), {})
 
     def test_foods(self):
         today = datetime.date.today().strftime('%Y-%m-%d')
@@ -350,6 +408,16 @@ class MiscTest(TestBase):
         self.common_api_test('get_badges', (), {}, (url,), {})
 
     def test_activities(self):
+        """
+        Test the getting/creating/deleting various activity related items.
+        Tests the following HTTP method/URLs:
+
+        GET https://api.fitbit.com/1/activities.json
+        POST https://api.fitbit.com/1/user/-/activities.json
+        GET https://api.fitbit.com/1/activities/FOOBAR.json
+        POST https://api.fitbit.com/1/user/-/activities/favorite/activity_id.json
+        DELETE https://api.fitbit.com/1/user/-/activities/favorite/activity_id.json
+        """
         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)
@@ -357,49 +425,93 @@ class MiscTest(TestBase):
         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)
+        url = URLBASE + "/-/activities/favorite/activity_id.json"
+        self.common_api_test('add_favorite_activity', ('activity_id',), {}, (url,), {'method': 'POST'})
+        self.common_api_test('delete_favorite_activity', ('activity_id',), {}, (url,), {'method': 'DELETE'})
 
+    def _test_get_bodyweight(self, base_date=None, user_id=None, period=None,
+                             end_date=None, expected_url=None):
+        """ Helper method for testing retrieving body weight measurements """
+        with mock.patch.object(self.fb, 'make_request') as make_request:
+            self.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)
+
+    def test_bodyweight(self):
+        """
+        Tests for retrieving body weight measurements.
+        https://wiki.fitbit.com/display/API/API-Get-Body-Weight
+        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
+        GET https://api.fitbit.com/1/user/BAR/body/log/weight/date/1992-05-12/1d.json
+        GET https://api.fitbit.com/1/user/-/body/log/weight/date/2015-02-26.json
+        """
         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,
+        self._test_get_bodyweight(
+            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),
+        self._test_get_bodyweight(
+            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,
+        self._test_get_bodyweight(
+            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'))
+        today = datetime.date.today().strftime('%Y-%m-%d')
+        self._test_get_bodyweight(
+            base_date=None, user_id=None, period=None, end_date=None,
+            expected_url=URLBASE + "/-/body/log/weight/date/%s.json" % today)
 
-    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)
+    def _test_get_bodyfat(self, base_date=None, user_id=None, period=None,
+                          end_date=None, expected_url=None):
+        """ Helper method for testing getting bodyfat measurements """
+        with mock.patch.object(self.fb, 'make_request') as make_request:
+            self.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)
 
+    def test_bodyfat(self):
+        """
+        Tests for retrieving bodyfat measurements.
+        https://wiki.fitbit.com/display/API/API-Get-Body-Fat
+        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
+        GET https://api.fitbit.com/1/user/BAR/body/log/fat/date/1992-05-12/1d.json
+        GET https://api.fitbit.com/1/user/-/body/log/fat/date/2015-02-26.json
+        """
         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,
+        self._test_get_bodyfat(
+            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),
+        self._test_get_bodyfat(
+            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,
+        self._test_get_bodyfat(
+            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'))
+        today = datetime.date.today().strftime('%Y-%m-%d')
+        self._test_get_bodyfat(
+            base_date=None, user_id=None, period=None, end_date=None,
+            expected_url=URLBASE + "/-/body/log/fat/date/%s.json" % today)
 
     def test_friends(self):
         url = URLBASE + "/-/friends.json"
@@ -424,20 +536,6 @@ class MiscTest(TestBase):
         self.common_api_test('accept_invite', ("FOO",), {}, (url,), {'data':{'accept': "true"}})
         self.common_api_test('reject_invite', ("FOO", ), {}, (url,), {'data':{'accept': "false"}})
 
-    def test_subscriptions(self):
-        url = URLBASE + "/-/apiSubscriptions.json"
-        self.common_api_test('list_subscriptions', (), {}, (url,), {})
-        url = URLBASE + "/-/FOO/apiSubscriptions.json"
-        self.common_api_test('list_subscriptions', ("FOO",), {}, (url,), {})
-        url = URLBASE + "/-/apiSubscriptions/SUBSCRIPTION_ID.json"
-        self.common_api_test('subscription', ("SUBSCRIPTION_ID", "SUBSCRIBER_ID"), {},
-                (url,), {'method': 'POST', 'headers': {'X-Fitbit-Subscriber-id': "SUBSCRIBER_ID"}})
-        self.common_api_test('subscription', ("SUBSCRIPTION_ID", "SUBSCRIBER_ID"), {'method': 'THROW'},
-            (url,), {'method': 'THROW', 'headers': {'X-Fitbit-Subscriber-id': "SUBSCRIBER_ID"}})
-        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,), {})
@@ -505,3 +603,144 @@ class MiscTest(TestBase):
                 },
             'method': 'POST'}
         )
+
+
+class SubscriptionsTest(TestBase):
+    """
+    Class for testing the Fitbit Subscriptions API:
+    https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API
+    """
+
+    def test_subscriptions(self):
+        """
+        Subscriptions tests. Tests the following methods/URLs:
+        GET https://api.fitbit.com/1/user/-/apiSubscriptions.json
+        GET https://api.fitbit.com/1/user/-/FOO/apiSubscriptions.json
+        POST https://api.fitbit.com/1/user/-/apiSubscriptions/SUBSCRIPTION_ID.json
+        POST https://api.fitbit.com/1/user/-/apiSubscriptions/SUBSCRIPTION_ID.json
+        POST https://api.fitbit.com/1/user/-/COLLECTION/apiSubscriptions/SUBSCRIPTION_ID-COLLECTION.json
+        """
+        url = URLBASE + "/-/apiSubscriptions.json"
+        self.common_api_test('list_subscriptions', (), {}, (url,), {})
+        url = URLBASE + "/-/FOO/apiSubscriptions.json"
+        self.common_api_test('list_subscriptions', ("FOO",), {}, (url,), {})
+        url = URLBASE + "/-/apiSubscriptions/SUBSCRIPTION_ID.json"
+        self.common_api_test('subscription', ("SUBSCRIPTION_ID", "SUBSCRIBER_ID"), {},
+                (url,), {'method': 'POST', 'headers': {'X-Fitbit-Subscriber-id': "SUBSCRIBER_ID"}})
+        self.common_api_test('subscription', ("SUBSCRIPTION_ID", "SUBSCRIBER_ID"), {'method': 'THROW'},
+            (url,), {'method': 'THROW', 'headers': {'X-Fitbit-Subscriber-id': "SUBSCRIBER_ID"}})
+        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"}})
+
+
+class PartnerAPITest(TestBase):
+    """
+    Class for testing the Fitbit Partner API:
+    https://wiki.fitbit.com/display/API/Fitbit+Partner+API
+    """
+
+    def _test_intraday_timeseries(self, resource, base_date, detail_level,
+                                  start_time, end_time, expected_url):
+        """ Helper method for intraday timeseries tests """
+        with mock.patch.object(self.fb, 'make_request') as make_request:
+            retval = self.fb.intraday_time_series(
+                resource, base_date, detail_level, start_time, end_time)
+        args, kwargs = make_request.call_args
+        self.assertEqual((expected_url,), args)
+
+    def test_intraday_timeseries(self):
+        """
+        Intraday Time Series tests:
+        https://wiki.fitbit.com/display/API/API-Get-Intraday-Time-Series
+
+        Tests the following methods/URLs:
+        GET https://api.fitbit.com/1/user/-/FOO/date/1918-05-11/1d/1min.json
+        GET https://api.fitbit.com/1/user/-/FOO/date/1918-05-11/1d/1min.json
+        GET https://api.fitbit.com/1/user/-/FOO/date/1918-05-11/1d/1min/time/03:56/15:07.json
+        GET https://api.fitbit.com/1/user/-/FOO/date/1918-05-11/1d/1min/time/3:56/15:07.json
+        """
+        resource = 'FOO'
+        base_date = '1918-05-11'
+
+        # detail_level must be valid
+        self.assertRaises(
+            ValueError,
+            self.fb.intraday_time_series,
+            resource,
+            base_date,
+            detail_level="xyz",
+            start_time=None,
+            end_time=None)
+
+        # provide end_time if start_time provided
+        self.assertRaises(
+            TypeError,
+            self.fb.intraday_time_series,
+            resource,
+            base_date,
+            detail_level="1min",
+            start_time='12:55',
+            end_time=None)
+        self.assertRaises(
+            TypeError,
+            self.fb.intraday_time_series,
+            resource,
+            base_date,
+            detail_level="1min",
+            start_time='12:55',
+            end_time='')
+
+        # provide start_time if end_time provided
+        self.assertRaises(
+            TypeError,
+            self.fb.intraday_time_series,
+            resource,
+            base_date,
+            detail_level="1min",
+            start_time=None,
+            end_time='12:55')
+        self.assertRaises(
+            TypeError,
+            self.fb.intraday_time_series,
+            resource,
+            base_date,
+            detail_level="1min",
+            start_time='',
+            end_time='12:55')
+
+        # Default
+        self._test_intraday_timeseries(
+            resource, base_date=base_date, detail_level='1min',
+            start_time=None, end_time=None,
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min.json")
+        # start_date can be a date object
+        self._test_intraday_timeseries(
+            resource, base_date=datetime.date(1918, 5, 11),
+            detail_level='1min', start_time=None, end_time=None,
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min.json")
+        # start_time can be a datetime object
+        self._test_intraday_timeseries(
+            resource, base_date=base_date, detail_level='1min',
+            start_time=datetime.time(3, 56), end_time='15:07',
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/03:56/15:07.json")
+        # end_time can be a datetime object
+        self._test_intraday_timeseries(
+            resource, base_date=base_date, detail_level='1min',
+            start_time='3:56', end_time=datetime.time(15, 7),
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/3:56/15:07.json")
+        # start_time can be a midnight datetime object
+        self._test_intraday_timeseries(
+            resource, base_date=base_date, detail_level='1min',
+            start_time=datetime.time(0, 0), end_time=datetime.time(15, 7),
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/00:00/15:07.json")
+        # end_time can be a midnight datetime object
+        self._test_intraday_timeseries(
+            resource, base_date=base_date, detail_level='1min',
+            start_time=datetime.time(3, 56), end_time=datetime.time(0, 0),
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/03:56/00:00.json")
+        # start_time and end_time can be a midnight datetime object
+        self._test_intraday_timeseries(
+            resource, base_date=base_date, detail_level='1min',
+            start_time=datetime.time(0, 0), end_time=datetime.time(0, 0),
+            expected_url=URLBASE + "/-/FOO/date/1918-05-11/1d/1min/time/00:00/00:00.json")
diff --git a/fitbit_tests/test_auth.py b/fitbit_tests/test_auth.py
index 8f83b75..6785ca7 100644
--- a/fitbit_tests/test_auth.py
+++ b/fitbit_tests/test_auth.py
@@ -1,66 +1,116 @@
 from unittest import TestCase
-from fitbit import Fitbit, FitbitOauthClient
+from fitbit import Fitbit, FitbitOauth2Client
 import mock
-from requests_oauthlib import OAuth1Session
+from requests_oauthlib import OAuth2Session
+from oauthlib.oauth2 import TokenExpiredError
 
-class AuthTest(TestCase):
+
+class Auth2Test(TestCase):
     """Add tests for auth part of API
     mock the oauth library calls to simulate various responses,
     make sure we call the right oauth calls, respond correctly based on the responses
     """
     client_kwargs = {
-        'client_key': '',
-        'client_secret': '',
-        'user_key': None,
-        'user_secret': None,
-        'callback_uri': 'CALLBACK_URL'
+        'client_id': 'fake_id',
+        'client_secret': 'fake_secret',
+        'callback_uri': 'fake_callback_url',
+        'scope': ['fake_scope1']
     }
 
-    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)
-        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)
-        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)
+        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])
 
     def test_authorize_token_url_with_parameters(self):
         # authorize_token_url calls oauth and returns a URL
-        client = FitbitOauthClient(**self.client_kwargs)
-        retval = client.authorize_token_url(display="touch")
-        self.assertTrue("display=touch" in retval)
+        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'])
 
     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 = {
+                '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'])
+        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['resource_owner_key'] = ''
-        kwargs['resource_owner_secret'] = ''
+        kwargs['access_token'] = 'fake_access_token'
+        kwargs['refresh_token'] = 'fake_refresh_token'
         fb = Fitbit(**kwargs)
-        fake_verifier = "FAKEVERIFIER"
-        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'
+        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.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)
+            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 tokenExpired exception
+        # 1. first call to _request causes a TokenExpired
+        # 2. the token_refresh call is faked
+        # 3. the second call to _request returns a valid value
+        kwargs = self.client_kwargs
+        kwargs['access_token'] = 'fake_access_token'
+        kwargs['refresh_token'] = 'fake_refresh_token'
+
+        fb = Fitbit(**kwargs)
+        with mock.patch.object(FitbitOauth2Client, '_request') as r:
+            r.side_effect = [TokenExpiredError, 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'])
+        self.assertEqual("fake_return_refresh_token", fb.client.token['refresh_token'])
+        self.assertEqual(1, rt.call_count)
+        self.assertEqual(2, r.call_count)
+
+    def test_auto_refresh_token_nonException(self):
+        # test of auto_refersh when the exception doesn't fire
+        # 1. first call to _request causes a 401 expired token response
+        # 2. the token_refresh call is faked
+        # 3. the second call to _request returns a valid value
+        kwargs = self.client_kwargs
+        kwargs['access_token'] = 'fake_access_token'
+        kwargs['refresh_token'] = 'fake_refresh_token'
+
+        fb = Fitbit(**kwargs)
+        with mock.patch.object(FitbitOauth2Client, '_request') as r:
+            r.side_effect = [fake_response(401, b'{"errors": [{"message": "Access token invalid or expired: some_token_goes_here", "errorType": "oauth", "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'])
+        self.assertEqual("fake_return_refresh_token", fb.client.token['refresh_token'])
+        self.assertEqual(1, rt.call_count)
+        self.assertEqual(2, r.call_count)
+
+
+class fake_response(object):
+    def __init__(self, code, text):
+        self.status_code = code
+        self.text = text
+        self.content = text
diff --git a/fitbit_tests/test_exceptions.py b/fitbit_tests/test_exceptions.py
index 2b87e9a..f656445 100644
--- a/fitbit_tests/test_exceptions.py
+++ b/fitbit_tests/test_exceptions.py
@@ -5,15 +5,16 @@ import sys
 from fitbit import Fitbit
 from fitbit import exceptions
 
+
 class ExceptionTest(unittest.TestCase):
     """
     Tests that certain response codes raise certain exceptions
     """
     client_kwargs = {
-        "client_key": "",
+        "client_id": "",
         "client_secret": "",
-        "user_key": None,
-        "user_secret": None,
+        "access_token": None,
+        "refresh_token": None
     }
 
     def test_response_ok(self):
@@ -36,7 +37,6 @@ class ExceptionTest(unittest.TestCase):
         r.status_code = 204
         f.user_profile_get()
 
-
     def test_response_auth(self):
         """
         This test checks how the client handles different auth responses, and
@@ -44,7 +44,7 @@ class ExceptionTest(unittest.TestCase):
         """
         r = mock.Mock(spec=requests.Response)
         r.status_code = 401
-        r.content = b"{'normal': 'resource'}"
+        r.content = b'{"normal": "resource"}'
 
         f = Fitbit(**self.client_kwargs)
         f.client._request = lambda *args, **kwargs: r
@@ -54,14 +54,14 @@ class ExceptionTest(unittest.TestCase):
         r.status_code = 403
         self.assertRaises(exceptions.HTTPForbidden, f.user_profile_get)
 
-
     def test_response_error(self):
         """
         Tests other HTTP errors
         """
         r = mock.Mock(spec=requests.Response)
-        r.content = b"{'normal': 'resource'}"
+        r.content = b'{"normal": "resource"}'
 
+        self.client_kwargs['oauth2'] = True
         f = Fitbit(**self.client_kwargs)
         f.client._request = lambda *args, **kwargs: r
 
diff --git a/gather_keys_cli.py b/gather_keys_cli.py
deleted file mode 100755
index c7b4523..0000000
--- a/gather_keys_cli.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/env python
-"""
-This was taken, and modified from python-oauth2/example/client.py,
-License reproduced below.
-
---------------------------
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-Example consumer. This is not recommended for production.
-Instead, you'll want to create your own subclass of OAuthClient
-or find one that works with your web framework.
-"""
-
-import os
-import pprint
-import sys
-import webbrowser
-
-from fitbit.api import FitbitOauthClient
-
-
-def gather_keys():
-    # setup
-    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 ...\n')
-    token = client.fetch_request_token()
-    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('\n* Obtain an access token ...\n')
-    token = client.fetch_access_token(verifier)
-    print('RESPONSE')
-    pp.pprint(token)
-    print('')
-
-
-if __name__ == '__main__':
-    if not (len(sys.argv) == 3):
-        print("Arguments 'client key', 'client secret' are required")
-        sys.exit(1)
-    CLIENT_KEY = sys.argv[1]
-    CLIENT_SECRET = sys.argv[2]
-
-    gather_keys()
-    print('Done.')
diff --git a/gather_keys_oauth2.py b/gather_keys_oauth2.py
new file mode 100755
index 0000000..7188644
--- /dev/null
+++ b/gather_keys_oauth2.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+import cherrypy
+import os
+import sys
+import threading
+import traceback
+import webbrowser
+
+from base64 import b64encode
+from fitbit.api import FitbitOauth2Client
+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)
+
+    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)
+        # Open the web browser in a new thread for command-line browser support
+        threading.Timer(1, webbrowser.open, args=(url,)).start()
+        cherrypy.quickstart(self)
+
+    @cherrypy.expose
+    def index(self, state, code=None, error=None):
+        """
+        Receive a Fitbit response containing a verification code. Use the code
+        to fetch the access_token.
+        """
+        error = None
+        if code:
+            try:
+                self.oauth.fetch_access_token(code, self.redirect_uri)
+            except MissingTokenError:
+                error = self._fmt_failure(
+                    'Missing access token parameter.</br>Please check that '
+                    'you are using the correct client_secret')
+            except MismatchingStateError:
+                error = self._fmt_failure('CSRF Warning! Mismatching state')
+        else:
+            error = self._fmt_failure('Unknown error while authenticating')
+        # Use a thread to shutdown cherrypy so we can return HTML first
+        self._shutdown_cherrypy()
+        return error if error else self.success_html
+
+    def _fmt_failure(self, message):
+        tb = traceback.format_tb(sys.exc_info()[2])
+        tb_html = '<pre>%s</pre>' % ('\n'.join(tb)) if tb else ''
+        return self.failure_html % (message, tb_html)
+
+    def _shutdown_cherrypy(self):
+        """ Shutdown cherrypy in one second, if it's running """
+        if cherrypy.engine.state == cherrypy.engine.states.STARTED:
+            threading.Timer(1, cherrypy.engine.exit).start()
+
+
+if __name__ == '__main__':
+
+    if not (len(sys.argv) == 3):
+        print("Arguments: client_id and client_secret")
+        sys.exit(1)
+
+    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'])
diff --git a/requirements/base.txt b/requirements/base.txt
index f5d86ee..faab5be 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,2 +1,2 @@
-python-dateutil>=1.5
-requests-oauthlib>=0.4.0
+python-dateutil>=1.5,<2.5
+requests-oauthlib>=0.4,<1.1
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 491e39f..27e4b56 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,5 +1,5 @@
 -r base.txt
 -r test.txt
 
-Sphinx==1.2.3
-tox==1.8.1
+cherrypy>=3.7,<3.9
+tox>=1.8,<2.2
diff --git a/requirements/test.txt b/requirements/test.txt
index 969f7a2..d5c6230 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,2 +1,3 @@
-mock==1.0.1
-coverage==3.7.1
+mock>=1.0,<1.4
+coverage>=3.7,<4.0
+Sphinx>=1.2,<1.4
diff --git a/setup.py b/setup.py
index 8dbbdb4..c17939a 100644
--- a/setup.py
+++ b/setup.py
@@ -5,8 +5,8 @@ import re
 
 from setuptools import setup
 
-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")]
+required = [line for line in open('requirements/base.txt').read().split("\n") if line != '']
+required_test = [line for line in open('requirements/test.txt').read().split("\n") if not line.startswith("-r") and line != '']
 
 fbinit = open('fitbit/__init__.py').read()
 author = re.search("__author__ = '([^']+)'", fbinit).group(1)
@@ -33,12 +33,11 @@ setup(
         'Natural Language :: English',
         'License :: OSI Approved :: Apache Software License',
         '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 :: 3.5',
         'Programming Language :: Python :: Implementation :: PyPy'
     ),
 )
diff --git a/tox.ini b/tox.ini
index e2f8462..279b114 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = pypy,py34,py33,py32,py27,py26
+envlist = pypy,py35,py34,py33,py27,docs
 
 [testenv]
 commands = coverage run --source=fitbit setup.py test
@@ -8,17 +8,18 @@ deps = -r{toxinidir}/requirements/test.txt
 [testenv:pypy]
 basepython = pypy
 
+[testenv:py35]
+basepython = python3.5
+
 [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
+[testenv:docs]
+basepython = python3.4
+commands = sphinx-build -W -b html docs docs/_build

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