From aee5a49b78e949a476db6cbb687e99ca87491429 Mon Sep 17 00:00:00 2001 From: Issac Kelly Date: Sat, 25 Feb 2012 17:16:49 -0800 Subject: [PATCH] First Commit of fitbit module. --- AUTHORS | 2 + LICENSE | 13 + TODO | 6 + fitbit/__init__.py | 21 ++ fitbit/api.py | 566 ++++++++++++++++++++++++++++++++++++++ fitbit/exceptions.py | 45 +++ fitbit/gather_keys_cli.py | 97 +++++++ fitbit/utils.py | 39 +++ requirements.txt | 3 + setup.py | 35 +++ 10 files changed, 827 insertions(+) create mode 100644 TODO create mode 100644 fitbit/__init__.py create mode 100644 fitbit/api.py create mode 100644 fitbit/exceptions.py create mode 100755 fitbit/gather_keys_cli.py create mode 100644 fitbit/utils.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/AUTHORS b/AUTHORS index e69de29..b397e6c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -0,0 +1,2 @@ +Issac Kelly (Kelly Creative Tech) +Percy Perez (ORCAS) diff --git a/LICENSE b/LICENSE index e69de29..0fe1585 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2012 Issac Kelly and ORCAS + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/TODO b/TODO new file mode 100644 index 0000000..cc97776 --- /dev/null +++ b/TODO @@ -0,0 +1,6 @@ +TODO + +* Public calls only based on consumer_key, (should work, untested) +* Change Units +* Docs +* Tests diff --git a/fitbit/__init__.py b/fitbit/__init__.py new file mode 100644 index 0000000..fa544f7 --- /dev/null +++ b/fitbit/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" +Fitbit API Library +------------------ + +:copyright: (c) 2012 by Issac Kelly. +:license: BSD, see LICENSE for more details. +""" +from .exceptions import BadResponse +from .api import Fitbit, FitbitConsumer, FitbitOauthClient + +# Meta. + +__title__ = 'fitbit' +__author__ = 'Issac Kelly and ORCAS' +__copyright__ = 'Copyright 2012 Issac Kelly' +__license__ = 'Apache 2.0' + +__version__ = '0.0.1' + +# Module namespace. diff --git a/fitbit/api.py b/fitbit/api.py new file mode 100644 index 0000000..f15090f --- /dev/null +++ b/fitbit/api.py @@ -0,0 +1,566 @@ +# -*- coding: utf-8 -*- +import oauth2 as oauth +import requests +import urlparse +import json +import datetime +import pdb #REMOVE! +import urllib + +from fitbit.exceptions import (BadResponse, DeleteError, HTTPBadRequest, + HTTPUnauthorized, HTTPForbidden, HTTPServerError, HTTPConflict, + HTTPNotFound) +from fitbit.utils import curry + + +class FitbitConsumer(oauth.Consumer): + pass + + +# example client using httplib with headers +class FitbitOauthClient(oauth.Client): + API_ENDPOINT = "https://api.fitbit.com" + AUTHORIZE_ENDPOINT = "https://www.fitbit.com" + API_VERSION = 1 + _signature_method = oauth.SignatureMethod_HMAC_SHA1() + + request_token_url = "%s/oauth/request_token" % API_ENDPOINT + access_token_url = "%s/oauth/access_token" % API_ENDPOINT + authorization_url = "%s/oauth/authorize" % AUTHORIZE_ENDPOINT + + def __init__(self, consumer_key, consumer_secret, user_key=None, user_secret=None, user_id=None, *args, **kwargs): + if user_key and user_secret: + self._token = oauth.Token(user_key, user_secret) + else: + # This allows public calls to be made + self._token = None + if user_id: + self.user_id = user_id + self._consumer = FitbitConsumer(consumer_key, consumer_secret) + super(FitbitOauthClient, self).__init__(self._consumer, *args, **kwargs) + + def _request(self, method, url, **kwargs): + """ + A simple wrapper around requests. + """ + return requests.request(method, url, **kwargs) + + def make_request(self, url, data={}, method=None, **kwargs): + """ + Builds and makes the Oauth Request, catches errors + + https://wiki.fitbit.com/display/API/API+Response+Format+And+Errors + """ + if not method: + method = 'POST' if data else 'GET' + request = oauth.Request.from_consumer_and_token(self._consumer, self._token, http_method=method, http_url=url, parameters=data) + request.sign_request(self._signature_method, self._consumer, self._token) + response = self._request(method, url, data=data, headers=request.to_header()) + + if response.status_code == 401: + raise HTTPUnauthorized(response) + elif response.status_code == 403: + raise HTTPForbidden(response) + elif response.status_code == 404: + raise HTTPNotFound(response) + elif response.status_code == 409: + raise HTTPConflict(response) + elif response.status_code >= 500: + raise HTTPServerError(response) + elif response.status_code >= 400: + raise HTTPBadRequest(response) + return response + + def fetch_request_token(self): + # via headers + # -> OAuthToken + request = oauth.Request.from_consumer_and_token(self._consumer, http_url=self.request_token_url) + request.sign_request(self._signature_method, self._consumer, None) + response = self._request(request.method, self.request_token_url, headers=request.to_header()) + return oauth.Token.from_string(response.content) + + def authorize_token_url(self, token): + request = oauth.Request.from_token_and_callback(token=token, http_url=self.authorization_url) + return request.to_url() + + #def authorize_token(self, token): + # # via url + # # -> typically just some okay response + # request = oauth.Request.from_token_and_callback(token=token, http_url=self.authorization_url) + # response = self._request(request.method, request.to_url(), headers=request.to_header()) + # return response.content + + def fetch_access_token(self, token, verifier): + request = oauth.Request.from_consumer_and_token(self._consumer, token, http_method='POST', http_url=self.access_token_url, parameters={'oauth_verifier': verifier}) + body = "oauth_verifier=%s" % verifier + response = self._request('POST', self.access_token_url, data=body, headers=request.to_header()) + if response.status_code != 200: + raise Exception("Invalid response %s." % response.content) # #TODO custom exceptions + params = urlparse.parse_qs(response.content, keep_blank_values=False) + self.user_id = params['encoded_user_id'][0] + self._token = oauth.Token.from_string(response.content) + return self._token + + +class Fitbit(object): + US = 'en_US' + METRIC = 'en_UK' + + API_ENDPOINT = "https://api.fitbit.com" + API_VERSION = 1 + DEBUG = True + + _resource_list = [ + 'body', + 'activities', + 'foods', + 'water', + 'sleep', + 'heart', + 'bp', + 'glucose', + ] + + _qualifiers = [ + 'recent', + 'favorite', + 'frequent', + ] + + def __init__(self, client, system=US): + self.client = client + self.SYSTEM = system + + # 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, curry(self._COLLECTION_RESOURCE, 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(self._DELETE_COLLECTION_RESOURCE, resource=resource)) + + for qualifier in self._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 + # serialization + headers = kwargs.get('headers', {}) + headers.update({'Accept-Language': self.SYSTEM}) + kwargs['headers'] = headers + response = self.client.make_request(*args, **kwargs) + if response.status_code in [202, 204]: + return True + try: + rep = json.loads(response.content) + except ValueError: + if self.DEBUG: + pdb.set_trace() + raise BadResponse + + if rep.get('errors', None) and self.DEBUG: + pdb.set_trace() + return rep + + def user_profile(self, user_id=None, data=None): + """ + Get or Set a user profile. You can get other user's profile information + by passing user_id, or you can set your user profile information by + passing a dictionary of attributes that will be updated. + + .. note: + This is not the same format that the GET comes back in, GET requests + are wrapped in {'user': } + + https://wiki.fitbit.com/display/API/API-Get-User-Info + https://wiki.fitbit.com/display/API/API-Update-User-Info + """ + if not user_id or data: + user_id = '-' + url = "%s/%s/user/%s/profile.json" % (self.API_ENDPOINT, self.API_VERSION, user_id) + return self.make_request(url, data) + + def _COLLECTION_RESOURCE(self, resource, date=None, user_id=None, data=None): + """ + Retreiving and logging of each type of collection data. + + Arguments: + resource, defined automatically via curry + [date] defaults to today + [user_id] defaults to current logged in user + [data] optional, include for creating a record, exclude for access + + https://wiki.fitbit.com/display/API/Fitbit+Resource+Access+API + """ + + if not date: + date = datetime.date.today() + if not user_id: + user_id = '-' + if not isinstance(date, basestring): + date = date.strftime('%Y-%m-%d') + + if not data: + url = "%s/%s/user/%s/%s/date/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + resource, + date, + ) + else: + data['date'] = date + url = "%s/%s/user/%s/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + resource, + ) + return self.make_request(url, data) + + def _DELETE_COLLECTION_RESOURCE(self, resource, log_id): + """ + deleting each type of collection data + + Arguments: + resource, defined automatically via curry + log_id, required, log entry to delete + """ + url = "%s/%s/user/-/%s/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + resource, + log_id, + ) + response = self.make_request(url, method='DELETE') + if response.status_code != 204: + raise DeleteError(response) + return response + + def time_series(self, resource, user_id=None, base_date='today', period=None, end_date=None): + """ + The time series is a LOT of methods, (documented at url below) so they + don't get their own method. They all follow the same patterns, and return + similar formats. + + Taking liberty, this assumes a base_date of today, the current user, and + a 1d period. + + https://wiki.fitbit.com/display/API/API-Get-Time-Series + """ + if not user_id: + user_id = '-' + + if period and end_date: + raise TypeError("Either end_date or period can be specified, not both") + elif end_date: + if not isinstance(end_date, basestring): + end = end_date.strftime('%Y-%m-%d') + else: + end = 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'") + end = period + + if not isinstance(base_date, basestring): + 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 + ) + return self.make_request(url) + + def activity_stats(self, user_id=None, qualifier=''): + """ + https://wiki.fitbit.com/display/API/API-Get-Activity-Stats + + https://wiki.fitbit.com/display/API/API-Get-Favorite-Activities + https://wiki.fitbit.com/display/API/API-Get-Recent-Activities + https://wiki.fitbit.com/display/API/API-Get-Frequent-Activities + """ + if not user_id: + user_id = '-' + + if qualifier: + if qualifier in self._activity_qualifiers: + qualifier = '/%s' % qualifier + else: + raise ValueError("Qualifier must be one of %s" + % ', '.join(self._activity_qualifiers)) + + url = "%s/%s/user/%s/activities%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + qualifier, + ) + return self.make_request(url) + + def _food_stats(self, user_id=None, qualifier=''): + """ + https://wiki.fitbit.com/display/API/API-Get-Recent-Foods + https://wiki.fitbit.com/display/API/API-Get-Frequent-Foods + https://wiki.fitbit.com/display/API/API-Get-Favorite-Foods + """ + if not user_id: + user_id = '-' + + url = "%s/%s/user/%s/foods/log/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + qualifier, + ) + return self.make_request(url) + + def add_favorite_activity(self, activity_id): + """ + 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, + ) + return self.make_request(url, method='POST') + + 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, + ) + return self.make_request(url, method='DELETE') + + def add_favorite_food(self, food_id): + """ + 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, + ) + return self.make_request(url, method='POST') + + def delete_favorite_food(self, food_id): + """ + 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, + ) + return self.make_request(url, method='DELETE') + + def create_food(self, data): + """ + https://wiki.fitbit.com/display/API/API-Create-Food + """ + url = "%s/%s/user/-/foods.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + ) + 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, + ) + 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, + ) + return self.make_request(url) + + def activities_list(self): + """ + https://wiki.fitbit.com/display/API/API-Browse-Activities + """ + url = "%s/%s/activities.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + ) + 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 + ) + return self.make_request(url) + + def search_foods(self, query): + """ + https://wiki.fitbit.com/display/API/API-Get-Activity + """ + url = "%s/%s/foods/search.json?query=%s" % ( + self.API_ENDPOINT, + self.API_VERSION, + urllib.urlencode(query) + ) + return self.make_request(url) + + def food_detail(self, food_id): + """ + https://wiki.fitbit.com/display/API/API-Get-Food + """ + url = "%s/%s/foods/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + food_id + ) + return self.make_request(url) + + def food_units(self): + """ + https://wiki.fitbit.com/display/API/API-Get-Food-Units + """ + url = "%s/%s/foods/units.json" % ( + self.API_ENDPOINT, + self.API_VERSION + ) + 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 + ) + return self.make_request(url) + + def get_friends_leaderboard(self, period): + """ + https://wiki.fitbit.com/display/API/API-Get-Friends-Leaderboard + """ + 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 + ) + return self.make_request(url) + + def invite_friend(self, data): + """ + https://wiki.fitbit.com/display/API/API-Create-Invite + """ + url = "%s/%s/user/-/friends/invitations.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + ) + return self.make_request(url, data=data) + + def invite_friend_by_email(self, email): + """ + Convenience Method for + https://wiki.fitbit.com/display/API/API-Create-Invite + """ + return self.invite_friend({'invitedUserEmail': email}) + + def invite_friend_by_userid(self, user_id): + """ + Convenience Method for + https://wiki.fitbit.com/display/API/API-Create-Invite + """ + return self.invite_friend({'invitedUserId': user_id}) + + def respond_to_invite(self, other_user_id, accept=True): + """ + https://wiki.fitbit.com/display/API/API-Accept-Invite + """ + url = "%s/%s/user/-/friends/invitations/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + other_user_id, + ) + accept = 'true' if accept else 'false' + return self.make_request(url, data={'accept': accept}) + + def accept_invite(self, other_user_id): + """ + Convenience method for respond_to_invite + """ + return self.respond_to_invite(other_user_id) + + def reject_invite(self, other_user_id): + """ + Convenience method for respond_to_invite + """ + return self.respond_to_invite(other_user_id, accept=False) + + def subscription(self, subscription_id, subscriber_id, collection=None, method='POST'): + """ + 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 + ) + return self.make_request( + url, + method=method, + headers={"X-Fitbit-Subscriber-id": subscriber_id} + ) + + def list_subscriptions(self, collection=''): + """ + 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, + ) + return self.make_request(url) + + @classmethod + def from_oauth_keys(self, consumer_key, consumer_secret, user_key=None, user_secret=None, user_id=None, system=US): + client = FitbitOauthClient(consumer_key, consumer_secret, user_key, user_secret, user_id) + return self(client, system) diff --git a/fitbit/exceptions.py b/fitbit/exceptions.py new file mode 100644 index 0000000..0cc2600 --- /dev/null +++ b/fitbit/exceptions.py @@ -0,0 +1,45 @@ + + +class BadResponse(Exception): + """ + Currently used if the response can't be json encoded, despite a .json extension + """ + pass + +class DeleteError(Exception): + """ + Used when a delete request did not return a 204 + """ + pass + +class HTTPException(Exception): + def __init__(self, response, *args, **kwargs): + import pdb + pdb.set_trace() + super(HTTPException, self).__init__(*args, **kwargs) + +class HTTPBadRequest(HTTPException): + pass + + +class HTTPUnauthorized(HTTPException): + pass + + +class HTTPForbidden(HTTPException): + pass + + +class HTTPServerError(HTTPException): + pass + + +class HTTPConflict(HTTPException): + """ + Used by Fitbit as rate limiter + """ + pass + + +class HTTPNotFound(HTTPException): + pass diff --git a/fitbit/gather_keys_cli.py b/fitbit/gather_keys_cli.py new file mode 100755 index 0000000..a11c2fb --- /dev/null +++ b/fitbit/gather_keys_cli.py @@ -0,0 +1,97 @@ +#!/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. +""" + +from api import FitbitOauthClient +import time +import oauth2 as oauth +import urlparse +import platform +import subprocess + + + +def gather_keys(): + # setup + print '** OAuth Python Library Example **' + client = FitbitOauthClient(CONSUMER_KEY, CONSUMER_SECRET) + + print '' + + # get request token + print '* Obtain a request token ...' + print '' + token = client.fetch_request_token() + print 'FROM RESPONSE' + print 'key: %s' % str(token.key) + print 'secret: %s' % str(token.secret) + print 'callback confirmed? %s' % str(token.callback_confirmed) + print '' + + print '* Authorize the request token in your browser' + print '' + if platform.mac_ver(): + subprocess.Popen(['open', client.authorize_token_url(token)]) + else: + print 'open: %s' % client.authorize_token_url(token) + print '' + verifier = raw_input('Verifier: ') + print verifier + print '' + + # get access token + print '* Obtain an access token ...' + print '' + print 'REQUEST (via headers)' + print '' + token = client.fetch_access_token(token, verifier) + print 'FROM RESPONSE' + print 'key: %s' % str(token.key) + print 'secret: %s' % str(token.secret) + print '' + + +def pause(): + print '' + time.sleep(1) + +if __name__ == '__main__': + import sys + + if not (len(sys.argv) == 3): + print "Arguments 'client key', 'client secret' are required" + sys.exit(1) + CONSUMER_KEY = sys.argv[1] + CONSUMER_SECRET = sys.argv[2] + + gather_keys() + print 'Done.' diff --git a/fitbit/utils.py b/fitbit/utils.py new file mode 100644 index 0000000..07eb04e --- /dev/null +++ b/fitbit/utils.py @@ -0,0 +1,39 @@ +""" +Curry was copied from Django's implementation. + +License is reproduced here. + +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + + +def curry(_curried_func, *args, **kwargs): + def _curried(*moreargs, **morekwargs): + return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs)) + return _curried diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6efb6f2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +https://github.com/dgouldin/python-oauth2/tarball/master +requests +python-dateutil==1.5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6e3cbe7 --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import fitbit + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +required = ['requests==0.10.1', 'python-dateutil==1.5'] + + +setup( + name='fitbit', + version=fitbit.__version__, + description='Fitbit API Wrapper.', + long_description=open('README.rst').read(), + author='Issac Kelly, ORCAS', + author_email='issac@kellycreativetech.com', + url='https://github.com/issackelly/python-fitbit', + packages=['fitbit'], + package_data={'': ['LICENSE']}, + include_package_data=True, + install_requires=required, + license='Apache 2.0', + classifiers=( + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: Apache 2.0', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + ), +)