From 71af3d4c119405ccd6baeb7272d1bf9aed2611ad Mon Sep 17 00:00:00 2001 From: jgysland Date: Mon, 17 Aug 2015 13:02:39 -0500 Subject: [PATCH] First commit --- pyfitbit.py | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 pyfitbit.py diff --git a/pyfitbit.py b/pyfitbit.py new file mode 100644 index 0000000..5535169 --- /dev/null +++ b/pyfitbit.py @@ -0,0 +1,151 @@ +"""pyfitbit gets Fitbit intraday data from fitbit.com.""" +# coding=utf-8 + +__author__ = 'jgysland' + +from robobrowser import RoboBrowser +import json +from datetime import datetime +import pandas as pd + + +class FitbitData(object): + """FibitData contains functions to log into fitbit.com and pull data from the api that feeds charts on the + dashboard and return them as a pandas.DataFrame with a pandas.DatetimeIndex at 15-minute intervals (the minimum + interval for which all metrics are available; heart-rate is available at 5-minute intervals. + + :param email: The email address used to log in to fitbit.com + :param passwd: The password associated with the email address. Defaults to None, which results in getpass.getpass + being called to prompt the user for the password without echoing. + :param metrics: A list of metrics from ['steps', 'distance', 'floors', 'active-minutes', 'calories-burned', + 'heart-rate']. Defaults to all metrics. + :param dt1: The first date for which to retrieve data (as ISO 8601, datetime.datetime, numpy.datetime64, or other + pandas.date_range()-compliant date-like object). Defaults to None, in which case dt1 is seven days prior to dt2. + :param dt2: The last date for which to retrieve data (with the same formatting considerations as dt1). Defaults + to None, in which case dt2 is the current date. + :param write_out: Write out a .csv of the pandas.DataFrame generated by FitbitData.get_data(). Default False. + :param filename: The filename to write to. Defaults to 'fitbit_export_%Y-%m-%d.csv' + """ + + def __init__(self, email, passwd=None, metrics=None, dt1=None, dt2=None, write_out=False, filename=None): + self.email = email + if passwd is None: + from getpass import getpass + self.passwd = getpass('Password for %s: ' % self.email) + else: + self.passwd = passwd + if metrics is None: + self.metrics = ['steps', 'distance', 'floors', 'active-minutes', 'calories-burned', 'heart-rate'] + else: + self.metrics = metrics + if dt2 is None: + self.dt2 = datetime.now() + else: + self.dt2 = dt2 + if dt1 is None: + from datetime import timedelta + + self.dt1 = self.dt2 - timedelta(days=7) + else: + self.dt1 = dt1 + self.browser = RoboBrowser(parser='lxml') + self.write_out = write_out + self.filename = filename + self.date, self.metric, self.data = (None, None, None) + + def login(self): + """Login to fitbit.com and return a RoboBrowser instance.""" + + self.browser.open('https://www.fitbit.com/login') + + login = self.browser.get_form('loginForm') + login['email'] = self.email + login['password'] = self.passwd + self.browser.submit_form(login) + + def get_daily_data(self): + """Get data on specified metric for specified date.""" + + id_data = {'template': '/ajaxTemplate.jsp', + 'serviceCalls': [{'name': 'activityTileData', + 'args': {'date': self.date, + 'dataTypes': self.metric}, + 'method': 'getIntradayData'}]} + csrf_token = {'csrfToken': self.browser.session.cookies['u'].split('|')[2]} + + self.browser.open('https://www.fitbit.com/ajaxapi', method='post', + data={'request': json.dumps(id_data), + 'csrfToken': json.dumps(csrf_token)}) + response = json.loads(self.browser.parsed.text) + + return response[0]['dataSets']['activity']['dataPoints'] + + def scrape(self): + """Get data on specified metric for all dates in FitbitData instance.""" + + date_range = pd.to_datetime(pd.date_range(self.dt1, self.dt2)) + if len(date_range) == 0: + from warnings import warn + + warn("dt1 is greater than dt2 resulting in 0 dates for which to pull data; dt1 and dt2 are being reversed.", + UserWarning) + date_range = pd.to_datetime(pd.date_range(self.dt2, self.dt1)) + dates = [d.strftime('%Y-%m-%d') for d in date_range] + data = [] + for d in dates: + self.date = d + data.append(self.get_daily_data()) + + return data + + def make_df(self): + """Return a pandas.DataFrame of the specified data, + optionally write to .csv file as specified in FitbitData instance.""" + + df = pd.concat([pd.DataFrame(d) for d in self.data], ignore_index=True) + df.dateTime = pd.to_datetime(df.dateTime) + df.set_index('dateTime', inplace=True) + + return df + + def get_data(self): + """Get data for all metrics and dates specified in FitbitData instance and return pandas.DataFrame of + :rtype : pandas.DataFrame + 15-minute observations.""" + + dfs = {} + self.login() + + for metric in self.metrics: + self.metric = metric + self.data = self.scrape() + df = self.make_df() + dfs[metric] = df + + df = pd.concat([dfs[k].groupby(pd.TimeGrouper('15Min')).bpm.median() if k == 'heart-rate' else + dfs[k].rename(columns={'value': k}) for k in dfs.keys()], + axis=1) + if self.filename is None: + self.filename = 'fitbit_export_%s.csv' % datetime.now().strftime('%Y-%m-%d') + if self.write_out is True: + df.to_csv(self.filename) + + return df + + +def get_fitbit_data(email, passwd=None, metrics=None, dt1=None, dt2=None, write_out=False, filename=None): + """Create a FitbitData instance and return pandas.DataFrame using FitbitData.get_data(). + + :param email: The email address used to log in to fitbit.com + :param passwd: The password associated with the email address. Defaults to None, which results in getpass.getpass + being called to prompt the user for the password without echoing. + :param metrics: A list of metrics from ['steps', 'distance', 'floors', 'active-minutes', 'calories-burned', + 'heart-rate']. Defaults to all metrics. + :param dt1: The first date for which to retrieve data (as ISO 8601, datetime.datetime, numpy.datetime64, or other + pandas.date_range()-compliant date-like object). Defaults to None, in which case dt1 is seven days prior to dt2. + :param dt2: The last date for which to retrieve data (with the same formatting considerations as dt1). Defaults + to None, in which case dt2 is the current date. + :param write_out: Write out a .csv of the pandas.DataFrame generated by FitbitData.get_data(). Default False. + :param filename: The filename to write to. Defaults to 'fitbit_export_%Y-%m-%d.csv'""" + fitbit = FitbitData(email, passwd, metrics, dt1, dt2, write_out, filename) + return fitbit.get_data() \ No newline at end of file