From d5a85143c1b5d6f47de2d21b12dc788aa16c0d29 Mon Sep 17 00:00:00 2001 From: Robert Buttery Date: Tue, 29 Oct 2024 17:11:48 -0300 Subject: [PATCH 1/2] initial commit --- .gitignore | 3 + fitbit/__init__.py | 2 +- fitbit/api.py | 225 ++++++++++++++++------- fitbit/auth.py | 24 +++ gather_keys_oauth2.py | 22 ++- playing_around.ipynb | 402 ++++++++++++++++++++++++++++++++++++++++++ requirements/base.txt | 2 + 7 files changed, 605 insertions(+), 75 deletions(-) create mode 100644 fitbit/auth.py create mode 100644 playing_around.ipynb diff --git a/.gitignore b/.gitignore index 8dff601..8d00fd7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ htmlcov # Editors .idea + +.env +token.json \ No newline at end of file diff --git a/fitbit/__init__.py b/fitbit/__init__.py index 0368d08..8a2b048 100644 --- a/fitbit/__init__.py +++ b/fitbit/__init__.py @@ -16,10 +16,10 @@ __author_email__ = 'bpitcher@orcasinc.com' __copyright__ = 'Copyright 2012-2017 ORCAS' __license__ = 'Apache 2.0' - __version__ = '0.3.1' __release__ = '0.3.1' # Module namespace. all_tests = [] + diff --git a/fitbit/api.py b/fitbit/api.py index 1b458b1..2d4351c 100644 --- a/fitbit/api.py +++ b/fitbit/api.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +# https://dev.fitbit.com/build/reference/web-api/explore/fitbit-web-api-swagger.json + import datetime import json import requests @@ -244,67 +245,14 @@ def __init__(self, client_id, client_secret, access_token=None, 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 - - method = kwargs.get('method', 'POST' if 'data' in kwargs else 'GET') - response = self.client.make_request(*args, **kwargs) - - if response.status_code == 202: - return True - if method == 'DELETE': - if response.status_code == 204: - return True - else: - raise exceptions.DeleteError(response) - try: - rep = json.loads(response.content.decode('utf8')) - except ValueError: - raise exceptions.BadResponse - - return rep - - def user_profile_get(self, user_id=None): - """ - Get a user profile. You can get other user's profile information - by passing user_id, or you can get the current user's by not passing - a user_id - - .. note: - This is not the same format that the GET comes back in, GET requests - are wrapped in {'user': } - - https://dev.fitbit.com/docs/user/ - """ - url = "{0}/{1}/user/{2}/profile.json".format(*self._get_common_args(user_id)) - return self.make_request(url) - - def user_profile_update(self, data): - """ - Set a user profile. 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://dev.fitbit.com/docs/user/#update-profile - """ - 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 - + 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') @@ -388,6 +336,113 @@ def _filter_nones(self, data): filtered_kwargs = list(filter(filter_nones, data.items())) return {} if not filtered_kwargs else dict(filtered_kwargs) + 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 + + method = kwargs.get('method', 'POST' if 'data' in kwargs else 'GET') + response = self.client.make_request(*args, **kwargs) + + if response.status_code == 202: + return True + if method == 'DELETE': + if response.status_code == 204: + return True + else: + raise exceptions.DeleteError(response) + try: + rep = json.loads(response.content.decode('utf8')) + except ValueError: + raise exceptions.BadResponse + + return rep + + # ------------------ + + def get_user_profile(self, user_id=None): + """ + Get a user profile. You can get other user's profile information + by passing user_id, or you can get the current user's by not passing + a user_id + + .. note: + This is not the same format that the GET comes back in, GET requests + are wrapped in {'user': } + + https://dev.fitbit.com/docs/user/ + """ + url = "{0}/{1}/user/{2}/profile.json".format(*self._get_common_args(user_id)) + return self.make_request(url) + + def update_user_profile(self, data): + """ + Set a user profile. 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://dev.fitbit.com/docs/user/#update-profile + """ + url = "{0}/{1}/user/-/profile.json".format(*self._get_common_args()) + return self.make_request(url, data) + + def get_user_badges(self, user_id=None): + """ + https://dev.fitbit.com/docs/friends/#badges + """ + url = "{0}/{1}/user/{2}/badges.json".format(*self._get_common_args(user_id)) + return self.make_request(url) + + # Active Zone Minutes (AZM) + def get_azm_time_series(self, user_id=None, date=None, period=None): + """ + https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/get-azm-timeseries-by-date/ + period = 1d | 7d | 30d | 1w | 1m | 3m | 6m | 1y + date = YYYY-MM-DD + url = /{0}/user/[user-id]/activities/active-zone-minutes/date/[date]/[period].json + """ + url = "{0}/{1}/user/{2}/activities/active-zone-minutes/date/{date}/{period}.json".format(*self._get_common_args(user_id), date=date, period=period) + return self.make_request(url) + + def get_azm_time_series_by_interval(self, start_date, end_date, user_id=None): + """ + Get Active Zone Minutes (AZM) time series data for a specified date range. + + https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/get-azm-timeseries-by-interval/ + + Arguments: + start_date -- The start date of the period, in the format YYYY-MM-DD or 'today' + end_date -- The end date of the period, in the format YYYY-MM-DD or 'today' + user_id -- The encoded ID of the user. Use None for current logged-in user. + + Returns: + dict containing the AZM data for each day in the specified range: + - activeZoneMinutes: Total count of active zone minutes + - fatBurnActiveZoneMinutes: Minutes in fat burn zone (1:1 ratio) + - cardioActiveZoneMinutes: Minutes in cardio zone (1:2 ratio) + - peakActiveZoneMinutes: Minutes in peak zone (1:2 ratio) + + Note: Maximum range is 1095 days + """ + start_date_str = self._get_date_string(start_date) + end_date_str = self._get_date_string(end_date) + + url = "{0}/{1}/user/{2}/activities/active-zone-minutes/date/{start_date}/{end_date}.json".format( + *self._get_common_args(user_id), + start_date=start_date_str, + end_date=end_date_str + ) + return self.make_request(url) + + # Activity + + + def body_fat_goal(self, fat=None): """ Implements the following APIs @@ -977,12 +1032,7 @@ def reject_invite(self, other_user_id): """ return self.respond_to_invite(other_user_id, accept=False) - def get_badges(self, user_id=None): - """ - https://dev.fitbit.com/docs/friends/#badges - """ - url = "{0}/{1}/user/{2}/badges.json".format(*self._get_common_args(user_id)) - return self.make_request(url) + def subscription(self, subscription_id, subscriber_id, collection=None, method='POST'): @@ -1011,3 +1061,44 @@ def list_subscriptions(self, collection=''): collection='/{0}'.format(collection) if collection else '' ) return self.make_request(url) + + def get_activity_time_series(self, resource, date, period, user_id=None): + """ + Get activity time series data for a specific resource by date and period. + + https://dev.fitbit.com/build/reference/web-api/activity-timeseries/get-activity-timeseries-by-date/ + + Arguments: + resource -- The resource to get data for. Supported values: + - activityCalories + - calories + - caloriesBMR + - distance + - elevation + - floors + - minutesSedentary + - minutesLightlyActive + - minutesFairlyActive + - minutesVeryActive + - steps + - swimming-strokes + Or tracker/* versions of above + date -- The end date of the period in YYYY-MM-DD format or 'today' + period -- The range period. Supported values: 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y + user_id -- The encoded ID of the user. Use None for current logged-in user. + + Returns: + dict containing the activity time series data + """ + if period not in ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y']: + raise ValueError("Period must be one of: 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y") + + date_string = self._get_date_string(date) + + url = "{0}/{1}/user/{2}/activities/{resource}/date/{date}/{period}.json".format( + *self._get_common_args(user_id), + resource=resource, + date=date_string, + period=period + ) + return self.make_request(url) diff --git a/fitbit/auth.py b/fitbit/auth.py new file mode 100644 index 0000000..b862e3e --- /dev/null +++ b/fitbit/auth.py @@ -0,0 +1,24 @@ +import fitbit.api as fitbit +import os +import json +from dotenv import load_dotenv +load_dotenv() + +client_id = os.environ.get("FITBIT_CLIENT_ID") +client_secret = os.environ.get("FITBIT_CLIENT_SECRET") + +def get_tokens(): + try: + with open('token.json', 'r') as f: + tokens = json.load(f) + access_token = tokens['access_token'] + refresh_token = tokens['refresh_token'] + return access_token, refresh_token + except FileNotFoundError: + print("token.json not found") + return None + +def get_fitbit_client(access_token, refresh_token): + return fitbit.Fitbit(client_id=client_id, client_secret=client_secret, + access_token=access_token, refresh_token=refresh_token) + diff --git a/gather_keys_oauth2.py b/gather_keys_oauth2.py index 39a19f8..f269c86 100755 --- a/gather_keys_oauth2.py +++ b/gather_keys_oauth2.py @@ -1,13 +1,14 @@ #!/usr/bin/env python import cherrypy +from dotenv import load_dotenv import os +load_dotenv() +import json import sys import threading import traceback import webbrowser - from urllib.parse import urlparse -from base64 import b64encode from fitbit.api import Fitbit from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError @@ -83,11 +84,13 @@ def _shutdown_cherrypy(self): 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() + client_id = os.getenv('FITBIT_CLIENT_ID') + client_secret = os.getenv('FITBIT_CLIENT_SECRET') + server = OAuth2Server(client_id, client_secret) + server.browser_authorize() + else: + server = OAuth2Server(*sys.argv[1:]) + server.browser_authorize() profile = server.fitbit.user_profile_get() print('You are authorized to access data for the user: {}'.format( @@ -96,3 +99,8 @@ def _shutdown_cherrypy(self): print('TOKEN\n=====\n') for key, value in server.fitbit.client.session.token.items(): print('{} = {}'.format(key, value)) + + with open('token.json', 'w') as f: + json.dump(server.fitbit.client.session.token, f) + +# bb0a2237af9edbb26e9f5880bbf7d61d447f2ecdb3f924ddc66063c8f90666d6 \ No newline at end of file diff --git a/playing_around.ipynb b/playing_around.ipynb new file mode 100644 index 0000000..1b714a8 --- /dev/null +++ b/playing_around.ipynb @@ -0,0 +1,402 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from fitbit.auth import get_tokens, get_fitbit_client\n", + "from datetime import datetime, timedelta\n", + "# %pip install pandas\n", + "import pandas as pd\n", + "import json\n", + "\n", + "access_token, refresh_token = get_tokens()\n", + "fitbit_client = get_fitbit_client(access_token, refresh_token)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "today = datetime.today().strftime('%Y-%m-%d')\n", + "last_week = (datetime.today() - timedelta(days=7)).strftime('%Y-%m-%d')\n", + "last_month = (datetime.today() - timedelta(days=30)).strftime('%Y-%m-%d')\n", + "last_year = (datetime.today() - timedelta(days=365)).strftime('%Y-%m-%d')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Annual Steps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.DataFrame(fitbit_client.get_activity_time_series_range(\n", + " resource='steps', \n", + " start_date=today,\n", + " end_date=last_year\n", + ")['activities-steps'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df['value'] = df['value'].astype(int)\n", + "df[df['value'] > 0]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Personal Best Stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "activity_stats = fitbit_client.activity_stats()\n", + "steps_best_day = activity_stats['best']['total']['steps']['value']\n", + "steps_best_day_date = activity_stats['best']['total']['steps']['date']\n", + "distance_lifetime = activity_stats['best']['total']['distance']['value']\n", + "\n", + "print(f\"Highest Step Count: {steps_best_day}\")\n", + "print(f\"Date Achieved: {steps_best_day_date}\")\n", + "print(f\"Total distance: {round(distance_lifetime, 2)} KM\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Personal Lifetime Stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lifetime_distance = activity_stats['lifetime']['total']['distance']\n", + "lifetime_steps = activity_stats['lifetime']['total']['steps']\n", + "\n", + "print(f\"Total distance: {round(lifetime_distance, 2)} KM\")\n", + "print(f\"Total steps: {lifetime_steps}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# User Profile" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "user_profile = fitbit_client.get_user_profile()['user']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "badges_df = pd.DataFrame(user_profile['topBadges'])\n", + "badges_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# not user_profile minus topBadges\n", + "user_profile_minus_badges = {k: v for k, v in user_profile.items() if k != 'topBadges'}\n", + "user_profile_df = pd.DataFrame(user_profile_minus_badges)\n", + "user_profile_df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sleep" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sleep_data = fitbit_client.get_sleep(date=datetime.today())\n", + "\n", + "# Sleep Data\n", + "sleep_df = pd.DataFrame(sleep_data['sleep'])\n", + "\n", + "# Sleep Stages Summary\n", + "sleep_stages_summary = sleep_data['summary']['stages']\n", + "deep_sleep_duration = sleep_stages_summary['deep']\n", + "light_sleep_duration = sleep_stages_summary['light']\n", + "rem_sleep_duration = sleep_stages_summary['rem']\n", + "wake_sleep_duration = sleep_stages_summary['wake']\n", + "\n", + "# append to sleep_df\n", + "sleep_df['deep_sleep_duration'] = deep_sleep_duration\n", + "sleep_df['light_sleep_duration'] = light_sleep_duration\n", + "sleep_df['rem_sleep_duration'] = rem_sleep_duration\n", + "sleep_df['wake_sleep_duration'] = wake_sleep_duration\n", + "\n", + "sleep_minute_df = pd.DataFrame(sleep_df['minuteData'][0])\n", + "\n", + "# try to remove minuteData from sleep_df \n", + "try: \n", + " sleep_df = sleep_df.drop(columns=['minuteData'])\n", + "except KeyError:\n", + " print(\"KeyError: minuteData not found\")\n", + " \n", + "sleep_df.T" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# group by value and count\n", + "sleep_minute_df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Recent Activities" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "# Resource Options\n", + "All Activity\tTracker Only Activity\n", + "----------------------------------\n", + "activityCalories\ttracker/activityCalories\n", + "calories\ttracker/calories\n", + "caloriesBMR\tN/A\n", + "distance\ttracker/distance\n", + "elevation\ttracker/elevation\n", + "floors\ttracker/floors\n", + "minutesSedentary\ttracker/minutesSedentary\n", + "minutesLightlyActive\ttracker/minutesLightlyActive\n", + "minutesFairlyActive\ttracker/minutesFairlyActive\n", + "minutesVeryActive\ttracker/minutesVeryActive\n", + "steps\ttracker/steps\n", + "swimming-strokes\tN/A\n", + "\"\"\"\n", + "\n", + "period = '7d' # 1d | 7d | 30d | 1w | 1m | 3m | 6m | 1y\n", + "activity_calories = fitbit_client.get_activity_time_series(resource='activityCalories', date=today, period=period)\n", + "calories = fitbit_client.get_activity_time_series(resource='calories', date=today, period=period)\n", + "distance = fitbit_client.get_activity_time_series(resource='distance', date=today, period=period)\n", + "elevation = fitbit_client.get_activity_time_series(resource='elevation', date=today, period=period)\n", + "floors = fitbit_client.get_activity_time_series(resource='floors', date=today, period=period)\n", + "minutes_sedentary = fitbit_client.get_activity_time_series(resource='minutesSedentary', date=today, period=period)\n", + "minutes_lightly_active = fitbit_client.get_activity_time_series(resource='minutesLightlyActive', date=today, period=period)\n", + "minutes_fairly_active = fitbit_client.get_activity_time_series(resource='minutesFairlyActive', date=today, period=period)\n", + "minutes_very_active = fitbit_client.get_activity_time_series(resource='minutesVeryActive', date=today, period=period)\n", + "steps = fitbit_client.get_activity_time_series(resource='steps', date=today, period=period)\n", + "swimming_strokes = fitbit_client.get_activity_time_series(resource='swimming-strokes', date=today, period=period)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
measurevaluedate
0activities-activityCalories12892024-10-23
1activities-calories29452024-10-23
2activities-distance2.9693577012024-10-23
3activities-elevation502024-10-23
4activities-floors52024-10-23
5activities-minutesSedentary11892024-10-23
6activities-minutesLightlyActive1872024-10-23
7activities-minutesFairlyActive422024-10-23
8activities-minutesVeryActive222024-10-23
9activities-steps62162024-10-23
10activities-swimming-strokes1532024-10-23
\n", + "
" + ], + "text/plain": [ + " measure value date\n", + "0 activities-activityCalories 1289 2024-10-23\n", + "1 activities-calories 2945 2024-10-23\n", + "2 activities-distance 2.969357701 2024-10-23\n", + "3 activities-elevation 50 2024-10-23\n", + "4 activities-floors 5 2024-10-23\n", + "5 activities-minutesSedentary 1189 2024-10-23\n", + "6 activities-minutesLightlyActive 187 2024-10-23\n", + "7 activities-minutesFairlyActive 42 2024-10-23\n", + "8 activities-minutesVeryActive 22 2024-10-23\n", + "9 activities-steps 6216 2024-10-23\n", + "10 activities-swimming-strokes 153 2024-10-23" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dfs = [activity_calories, calories, distance, elevation, floors, minutes_sedentary, minutes_lightly_active, minutes_fairly_active, minutes_very_active, steps, swimming_strokes]\n", + "\n", + "combined_df = []\n", + "for row in dfs:\n", + " name = list(row.keys())[0]\n", + "# new_row = {\n", + "# 'measure': name,\n", + "# 'value': row[name][0]['value'],\n", + "# 'date': row[name][0]['dateTime']\n", + "# }\n", + "# combined_df.append(new_row)\n", + " \n", + "# combined_df = pd.DataFrame(combined_df)\n", + "# combined_df" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements/base.txt b/requirements/base.txt index 1331f7b..a7d04ca 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,2 +1,4 @@ python-dateutil>=1.5 requests-oauthlib>=0.7 +setuptools +python-dotenv \ No newline at end of file From 91baa7314d6426dd80f3d0325b4e3f9601b96ab0 Mon Sep 17 00:00:00 2001 From: Robert Buttery Date: Wed, 30 Oct 2024 19:04:08 -0300 Subject: [PATCH 2/2] commit --- fitbit/api.py | 189 +------------------------------- fitbit/auth.py | 183 +++++++++++++++++++++++++++++-- gather_keys_oauth2.py | 59 ++++------ playing_around.ipynb | 246 ++++++++++++++++++------------------------ requirements/base.txt | 3 +- requirements/dev.txt | 2 +- 6 files changed, 306 insertions(+), 376 deletions(-) diff --git a/fitbit/api.py b/fitbit/api.py index 2d4351c..28c54bf 100644 --- a/fitbit/api.py +++ b/fitbit/api.py @@ -16,151 +16,7 @@ from . import exceptions from .compliance import fitbit_compliance_fix from .utils import curry - - -class FitbitOauth2Client(object): - API_ENDPOINT = "https://api.fitbit.com" - AUTHORIZE_ENDPOINT = "https://www.fitbit.com" - API_VERSION = 1 - - 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_id, client_secret, access_token=None, - refresh_token=None, expires_at=None, refresh_cb=None, - redirect_uri=None, *args, **kwargs): - """ - Create a FitbitOauth2Client object. Specify the first 7 parameters if - you have them to access user data. Specify just the first 2 parameters - 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.client_id, self.client_secret = client_id, client_secret - token = {} - if access_token and refresh_token: - token.update({ - 'access_token': access_token, - 'refresh_token': refresh_token - }) - if expires_at: - token['expires_at'] = expires_at - self.session = fitbit_compliance_fix(OAuth2Session( - client_id, - auto_refresh_url=self.refresh_token_url, - token_updater=refresh_cb, - token=token, - redirect_uri=redirect_uri, - )) - self.timeout = kwargs.get("timeout", None) - - def _request(self, method, url, **kwargs): - """ - A simple wrapper around requests. - """ - if self.timeout is not None and 'timeout' not in kwargs: - kwargs['timeout'] = self.timeout - - try: - response = self.session.request(method, url, **kwargs) - - # If our current token has no expires_at, or something manages to slip - # through that check - if response.status_code == 401: - d = json.loads(response.content.decode('utf8')) - if d['errors'][0]['errorType'] == 'expired_token': - self.refresh_token() - response = self.session.request(method, url, **kwargs) - - return response - except requests.Timeout as e: - raise exceptions.Timeout(*e.args) - - def make_request(self, url, data=None, method=None, **kwargs): - """ - Builds and makes the OAuth2 Request, catches errors - - https://dev.fitbit.com/docs/oauth2/#authorization-errors - """ - data = data or {} - method = method or ('POST' if data else 'GET') - response = self._request( - method, - url, - data=data, - client_id=self.client_id, - client_secret=self.client_secret, - **kwargs - ) - - exceptions.detect_and_raise_error(response) - - return response - - 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. - - scope: pemissions that that are being requested [default ask all] - - redirect_uri: url to which the response will posted. required here - unless you specify only one Callback URL on the fitbit app or - you already passed it to the constructor - for more info see https://dev.fitbit.com/docs/oauth2/ - """ - - self.session.scope = scope or [ - "activity", - "nutrition", - "heartrate", - "location", - "nutrition", - "profile", - "settings", - "sleep", - "social", - "weight", - ] - - if redirect_uri: - self.session.redirect_uri = redirect_uri - - return self.session.authorization_url(self.authorization_url, **kwargs) - - def fetch_access_token(self, code, redirect_uri=None): - - """Step 2: Given the code from fitbit from step 1, call - fitbit again and returns an access token object. Extract the needed - information from that and save it to use in future API calls. - the token is internally saved - """ - if redirect_uri: - self.session.redirect_uri = redirect_uri - return self.session.fetch_token( - self.access_token_url, - username=self.client_id, - password=self.client_secret, - client_secret=self.client_secret, - code=code) - - def refresh_token(self): - """Step 3: obtains a new access_token from the the refresh token - obtained in step 2. Only do the refresh if there is `token_updater(),` - which saves the token. - """ - token = {} - if self.session.token_updater: - token = self.session.refresh_token( - self.refresh_token_url, - auth=HTTPBasicAuth(self.client_id, self.client_secret) - ) - self.session.token_updater(token) - - return token +from .auth import FitbitOauth2Client class Fitbit(object): @@ -1061,44 +917,5 @@ def list_subscriptions(self, collection=''): collection='/{0}'.format(collection) if collection else '' ) return self.make_request(url) - - def get_activity_time_series(self, resource, date, period, user_id=None): - """ - Get activity time series data for a specific resource by date and period. - - https://dev.fitbit.com/build/reference/web-api/activity-timeseries/get-activity-timeseries-by-date/ - - Arguments: - resource -- The resource to get data for. Supported values: - - activityCalories - - calories - - caloriesBMR - - distance - - elevation - - floors - - minutesSedentary - - minutesLightlyActive - - minutesFairlyActive - - minutesVeryActive - - steps - - swimming-strokes - Or tracker/* versions of above - date -- The end date of the period in YYYY-MM-DD format or 'today' - period -- The range period. Supported values: 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y - user_id -- The encoded ID of the user. Use None for current logged-in user. - - Returns: - dict containing the activity time series data - """ - if period not in ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y']: - raise ValueError("Period must be one of: 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y") - - date_string = self._get_date_string(date) - - url = "{0}/{1}/user/{2}/activities/{resource}/date/{date}/{period}.json".format( - *self._get_common_args(user_id), - resource=resource, - date=date_string, - period=period - ) - return self.make_request(url) + + \ No newline at end of file diff --git a/fitbit/auth.py b/fitbit/auth.py index b862e3e..5a2623b 100644 --- a/fitbit/auth.py +++ b/fitbit/auth.py @@ -1,13 +1,15 @@ -import fitbit.api as fitbit -import os import json +from fitbit.compliance import fitbit_compliance_fix +from requests.auth import HTTPBasicAuth +from requests_oauthlib import OAuth2Session +import requests +import fitbit.exceptions +from typing import Tuple +import os from dotenv import load_dotenv load_dotenv() -client_id = os.environ.get("FITBIT_CLIENT_ID") -client_secret = os.environ.get("FITBIT_CLIENT_SECRET") - -def get_tokens(): +def get_local_tokens() -> Tuple[str, str] | None: try: with open('token.json', 'r') as f: tokens = json.load(f) @@ -18,7 +20,170 @@ def get_tokens(): print("token.json not found") return None -def get_fitbit_client(access_token, refresh_token): - return fitbit.Fitbit(client_id=client_id, client_secret=client_secret, - access_token=access_token, refresh_token=refresh_token) +class FitbitOauth2Client(object): + API_ENDPOINT = "https://api.fitbit.com" + AUTHORIZE_ENDPOINT = "https://www.fitbit.com" + API_VERSION = 1 + + request_token_url = f"{API_ENDPOINT}/oauth2/token" + authorization_url = f"{AUTHORIZE_ENDPOINT}/oauth2/authorize" + access_token_url = request_token_url + refresh_token_url = request_token_url + + def __init__(self, client_id, client_secret, access_token=None, + refresh_token=None, expires_at=None, refresh_cb=None, + redirect_uri=None, *args, **kwargs): + """ + Create a FitbitOauth2Client object. Specify the first 7 parameters if + you have them to access user data. Specify just the first 2 parameters + 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.client_id, self.client_secret = client_id, client_secret + token = {} + if access_token and refresh_token: + token.update({ + 'access_token': access_token, + 'refresh_token': refresh_token + }) + if expires_at: + token['expires_at'] = expires_at + self.session = fitbit_compliance_fix(OAuth2Session( + client_id, + auto_refresh_url=self.refresh_token_url, + token_updater=refresh_cb, + token=token, + redirect_uri=redirect_uri, + )) + self.timeout = kwargs.get("timeout", None) + + def _request(self, method, url, **kwargs): + """ + A simple wrapper around requests. + """ + if self.timeout is not None and 'timeout' not in kwargs: + kwargs['timeout'] = self.timeout + + try: + response = self.session.request(method, url, **kwargs) + + # If our current token has no expires_at, or something manages to slip + # through that check + if response.status_code == 401: + d = json.loads(response.content.decode('utf8')) + if d['errors'][0]['errorType'] == 'expired_token': + self.refresh_token() + response = self.session.request(method, url, **kwargs) + + return response + except requests.Timeout as e: + raise exceptions.Timeout(*e.args) + + def make_request(self, url, data=None, method=None, **kwargs): + """ + Builds and makes the OAuth2 Request, catches errors + + https://dev.fitbit.com/docs/oauth2/#authorization-errors + """ + data = data or {} + method = method or ('POST' if data else 'GET') + response = self._request( + method, + url, + data=data, + client_id=self.client_id, + client_secret=self.client_secret, + **kwargs + ) + + exceptions.detect_and_raise_error(response) + + return response + + 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. + - scope: pemissions that that are being requested [default ask all] + - redirect_uri: url to which the response will posted. required here + unless you specify only one Callback URL on the fitbit app or + you already passed it to the constructor + for more info see https://dev.fitbit.com/docs/oauth2/ + """ + + self.session.scope = scope or [ + "activity", + "nutrition", + "heartrate", + "location", + "nutrition", + "profile", + "settings", + "sleep", + "social", + "weight", + ] + + if redirect_uri: + self.session.redirect_uri = redirect_uri + + return self.session.authorization_url(self.authorization_url, **kwargs) + + def fetch_access_token(self, code, redirect_uri=None): + + """Step 2: Given the code from fitbit from step 1, call + fitbit again and returns an access token object. Extract the needed + information from that and save it to use in future API calls. + the token is internally saved + """ + if redirect_uri: + self.session.redirect_uri = redirect_uri + return self.session.fetch_token( + self.access_token_url, + username=self.client_id, + password=self.client_secret, + client_secret=self.client_secret, + code=code) + + def refresh_token(self): + """Step 3: obtains a new access_token from the the refresh token + obtained in step 2. Only do the refresh if there is `token_updater(),` + which saves the token. + """ + token = {} + if self.session.token_updater: + token = self.session.refresh_token( + self.refresh_token_url, + auth=HTTPBasicAuth(self.client_id, self.client_secret) + ) + self.session.token_updater(token) + + return token + + +if __name__ == "__main__": + + access_token, refresh_token = get_local_tokens() + client_id = os.getenv('FITBIT_CLIENT_ID') + client_secret = os.getenv('FITBIT_CLIENT_SECRET') + + client = FitbitOauth2Client( + client_id, + client_secret, + access_token=access_token, + refresh_token=refresh_token + ) + # print(client.session.token) + + # print(client.session.get('https://api.fitbit.com/1/user/-/profile.json').json()) + + auth_url = client.authorize_token_url( + scope='activity nutrition heartrate location nutrition profile settings sleep social weight', + redirect_uri='http://localhost:8080' + ) + print(auth_url) diff --git a/gather_keys_oauth2.py b/gather_keys_oauth2.py index f269c86..e1c28af 100755 --- a/gather_keys_oauth2.py +++ b/gather_keys_oauth2.py @@ -1,17 +1,16 @@ #!/usr/bin/env python import cherrypy -from dotenv import load_dotenv -import os -load_dotenv() import json +import os import sys import threading import traceback import webbrowser -from urllib.parse import urlparse -from fitbit.api import Fitbit +from fitbit.auth import FitbitOauth2Client from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError +from dotenv import load_dotenv +load_dotenv() class OAuth2Server: def __init__(self, client_id, client_secret, @@ -22,34 +21,24 @@ def __init__(self, client_id, client_secret,

You can close this window

""" self.failure_html = """

ERROR: %s


You can close this window

%s""" - - self.fitbit = Fitbit( + self.fitbit = FitbitOauth2Client( client_id, client_secret, - redirect_uri=redirect_uri, - timeout=10, + redirect_uri=redirect_uri ) - self.redirect_uri = redirect_uri - def browser_authorize(self): """ Open a browser to the authorization url and spool up a CherryPy server to accept the response """ - url, _ = self.fitbit.client.authorize_token_url() - # Open the web browser in a new thread for command-line browser support + url, _ = self.fitbit.authorize_token_url() + # Open the web browser in a new thread for command-line python threading.Timer(1, webbrowser.open, args=(url,)).start() - - # Same with redirect_uri hostname and port. - urlparams = urlparse(self.redirect_uri) - cherrypy.config.update({'server.socket_host': urlparams.hostname, - 'server.socket_port': urlparams.port}) - cherrypy.quickstart(self) @cherrypy.expose - def index(self, state, code=None, error=None): + def index(self, state=None, code=None, error=None): """ Receive a Fitbit response containing a verification code. Use the code to fetch the access_token. @@ -57,7 +46,7 @@ def index(self, state, code=None, error=None): error = None if code: try: - self.fitbit.client.fetch_access_token(code) + self.fitbit.fetch_access_token(code) except MissingTokenError: error = self._fmt_failure( 'Missing access token parameter.
Please check that ' @@ -82,25 +71,15 @@ def _shutdown_cherrypy(self): if __name__ == '__main__': + client_id = os.getenv('FITBIT_CLIENT_ID') + client_secret = os.getenv('FITBIT_CLIENT_SECRET') + + server = OAuth2Server( + client_id, + client_secret + ) + server.browser_authorize() - if not (len(sys.argv) == 3): - client_id = os.getenv('FITBIT_CLIENT_ID') - client_secret = os.getenv('FITBIT_CLIENT_SECRET') - server = OAuth2Server(client_id, client_secret) - server.browser_authorize() - else: - server = OAuth2Server(*sys.argv[1:]) - server.browser_authorize() - - profile = server.fitbit.user_profile_get() - print('You are authorized to access data for the user: {}'.format( - profile['user']['fullName'])) - - print('TOKEN\n=====\n') - for key, value in server.fitbit.client.session.token.items(): - print('{} = {}'.format(key, value)) with open('token.json', 'w') as f: - json.dump(server.fitbit.client.session.token, f) - -# bb0a2237af9edbb26e9f5880bbf7d61d447f2ecdb3f924ddc66063c8f90666d6 \ No newline at end of file + json.dump(server.fitbit.session.token, f) diff --git a/playing_around.ipynb b/playing_around.ipynb index 1b714a8..db726f3 100644 --- a/playing_around.ipynb +++ b/playing_around.ipynb @@ -6,14 +6,18 @@ "metadata": {}, "outputs": [], "source": [ - "from fitbit.auth import get_tokens, get_fitbit_client\n", + "from fitbit.api import Fitbit\n", "from datetime import datetime, timedelta\n", - "# %pip install pandas\n", - "import pandas as pd\n", - "import json\n", + "from dotenv import load_dotenv\n", + "import os\n", + "from fitbit.auth import get_local_tokens\n", + "load_dotenv()\n", + "\n", + "client_id = os.getenv('FITBIT_CLIENT_ID')\n", + "client_secret = os.getenv('FITBIT_CLIENT_SECRET')\n", "\n", - "access_token, refresh_token = get_tokens()\n", - "fitbit_client = get_fitbit_client(access_token, refresh_token)" + "access_token, refresh_token = get_local_tokens()\n", + "fitbit_client = Fitbit(client_id, client_secret, access_token=access_token, refresh_token=refresh_token)\n" ] }, { @@ -37,9 +41,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "TypeError", + "evalue": "Fitbit.get_activity_time_series_range() got an unexpected keyword argument 'resource'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[3], line 3\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mpd\u001b[39;00m\n\u001b[1;32m----> 3\u001b[0m df \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mDataFrame(\u001b[43mfitbit_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_activity_time_series_range\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mresource\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43msteps\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\n\u001b[0;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mstart_date\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtoday\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43mend_date\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlast_year\u001b[49m\n\u001b[0;32m 7\u001b[0m \u001b[43m)\u001b[49m[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mactivities-steps\u001b[39m\u001b[38;5;124m'\u001b[39m])\n", + "\u001b[1;31mTypeError\u001b[0m: Fitbit.get_activity_time_series_range() got an unexpected keyword argument 'resource'" + ] + } + ], "source": [ "import pandas as pd\n", "\n", @@ -112,9 +128,24 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'exceptions' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[4], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m user_profile \u001b[38;5;241m=\u001b[39m \u001b[43mfitbit_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_user_profile\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser\u001b[39m\u001b[38;5;124m'\u001b[39m]\n", + "File \u001b[1;32mc:\\Projects\\python-fitbit\\fitbit\\api.py:234\u001b[0m, in \u001b[0;36mFitbit.get_user_profile\u001b[1;34m(self, user_id)\u001b[0m\n\u001b[0;32m 222\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 223\u001b[0m \u001b[38;5;124;03mGet a user profile. You can get other user's profile information\u001b[39;00m\n\u001b[0;32m 224\u001b[0m \u001b[38;5;124;03mby passing user_id, or you can get the current user's by not passing\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 231\u001b[0m \u001b[38;5;124;03mhttps://dev.fitbit.com/docs/user/\u001b[39;00m\n\u001b[0;32m 232\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 233\u001b[0m url \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{0}\u001b[39;00m\u001b[38;5;124m/\u001b[39m\u001b[38;5;132;01m{1}\u001b[39;00m\u001b[38;5;124m/user/\u001b[39m\u001b[38;5;132;01m{2}\u001b[39;00m\u001b[38;5;124m/profile.json\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\u001b[38;5;241m*\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_common_args(user_id))\n\u001b[1;32m--> 234\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmake_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43murl\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Projects\\python-fitbit\\fitbit\\api.py:203\u001b[0m, in \u001b[0;36mFitbit.make_request\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 200\u001b[0m kwargs[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mheaders\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m headers\n\u001b[0;32m 202\u001b[0m method \u001b[38;5;241m=\u001b[39m kwargs\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mmethod\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mPOST\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;129;01min\u001b[39;00m kwargs \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mGET\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m--> 203\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmake_request\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 205\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m response\u001b[38;5;241m.\u001b[39mstatus_code \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m202\u001b[39m:\n\u001b[0;32m 206\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m\n", + "File \u001b[1;32mc:\\Projects\\python-fitbit\\fitbit\\auth.py:102\u001b[0m, in \u001b[0;36mFitbitOauth2Client.make_request\u001b[1;34m(self, url, data, method, **kwargs)\u001b[0m\n\u001b[0;32m 92\u001b[0m method \u001b[38;5;241m=\u001b[39m method \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mPOST\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m data \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mGET\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m 93\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_request(\n\u001b[0;32m 94\u001b[0m method,\n\u001b[0;32m 95\u001b[0m url,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 99\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[0;32m 100\u001b[0m )\n\u001b[1;32m--> 102\u001b[0m \u001b[43mexceptions\u001b[49m\u001b[38;5;241m.\u001b[39mdetect_and_raise_error(response)\n\u001b[0;32m 104\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m response\n", + "\u001b[1;31mNameError\u001b[0m: name 'exceptions' is not defined" + ] + } + ], "source": [ "user_profile = fitbit_client.get_user_profile()['user']" ] @@ -202,7 +233,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 74, "metadata": {}, "outputs": [], "source": [ @@ -224,7 +255,7 @@ "swimming-strokes\tN/A\n", "\"\"\"\n", "\n", - "period = '7d' # 1d | 7d | 30d | 1w | 1m | 3m | 6m | 1y\n", + "period = '1d' # 1d | 7d | 30d | 1w | 1m | 3m | 6m | 1y\n", "activity_calories = fitbit_client.get_activity_time_series(resource='activityCalories', date=today, period=period)\n", "calories = fitbit_client.get_activity_time_series(resource='calories', date=today, period=period)\n", "distance = fitbit_client.get_activity_time_series(resource='distance', date=today, period=period)\n", @@ -240,142 +271,79 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
measurevaluedate
0activities-activityCalories12892024-10-23
1activities-calories29452024-10-23
2activities-distance2.9693577012024-10-23
3activities-elevation502024-10-23
4activities-floors52024-10-23
5activities-minutesSedentary11892024-10-23
6activities-minutesLightlyActive1872024-10-23
7activities-minutesFairlyActive422024-10-23
8activities-minutesVeryActive222024-10-23
9activities-steps62162024-10-23
10activities-swimming-strokes1532024-10-23
\n", - "
" - ], - "text/plain": [ - " measure value date\n", - "0 activities-activityCalories 1289 2024-10-23\n", - "1 activities-calories 2945 2024-10-23\n", - "2 activities-distance 2.969357701 2024-10-23\n", - "3 activities-elevation 50 2024-10-23\n", - "4 activities-floors 5 2024-10-23\n", - "5 activities-minutesSedentary 1189 2024-10-23\n", - "6 activities-minutesLightlyActive 187 2024-10-23\n", - "7 activities-minutesFairlyActive 42 2024-10-23\n", - "8 activities-minutesVeryActive 22 2024-10-23\n", - "9 activities-steps 6216 2024-10-23\n", - "10 activities-swimming-strokes 153 2024-10-23" - ] - }, - "execution_count": 70, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dfs = [activity_calories, calories, distance, elevation, floors, minutes_sedentary, minutes_lightly_active, minutes_fairly_active, minutes_very_active, steps, swimming_strokes]\n", "\n", "combined_df = []\n", "for row in dfs:\n", " name = list(row.keys())[0]\n", - "# new_row = {\n", - "# 'measure': name,\n", - "# 'value': row[name][0]['value'],\n", - "# 'date': row[name][0]['dateTime']\n", - "# }\n", - "# combined_df.append(new_row)\n", + " new_row = {\n", + " 'measure': name,\n", + " 'value': row[name][0]['value'],\n", + " 'date': row[name][0]['dateTime']\n", + " }\n", + " combined_df.append(new_row)\n", " \n", - "# combined_df = pd.DataFrame(combined_df)\n", - "# combined_df" + "combined_df = pd.DataFrame(combined_df)\n", + "combined_df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Weight\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "weight_data = fitbit_client.get_weight_log(date=today)\n", + "weight_data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "url = 'https://dev.fitbit.com/build/reference/web-api/explore/fitbit-web-api-swagger.json'\n", + "json_schema = requests.get(url=url).json()\n", + "json_schema.keys()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(json_schema['paths'])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "[json_schema['paths'][x] for x in json_schema['paths'].keys()]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/requirements/base.txt b/requirements/base.txt index a7d04ca..4af5f61 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,5 @@ python-dateutil>=1.5 requests-oauthlib>=0.7 setuptools -python-dotenv \ No newline at end of file +python-dotenv +httpx \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 27e4b56..8adaec0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r base.txt -r test.txt -cherrypy>=3.7,<3.9 +cherrypy>=3.7 tox>=1.8,<2.2