fitbit-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fitbit_cli-0.1.0/MANIFEST.in +3 -0
- fitbit_cli-0.1.0/PKG-INFO +108 -0
- fitbit_cli-0.1.0/README.md +78 -0
- fitbit_cli-0.1.0/fitbit_cli/__init__.py +6 -0
- fitbit_cli-0.1.0/fitbit_cli/cli.py +106 -0
- fitbit_cli-0.1.0/fitbit_cli/exceptions.py +20 -0
- fitbit_cli-0.1.0/fitbit_cli/fitbit_api.py +142 -0
- fitbit_cli-0.1.0/fitbit_cli/fitbit_init.py +165 -0
- fitbit_cli-0.1.0/fitbit_cli/formatter.py +147 -0
- fitbit_cli-0.1.0/fitbit_cli/main.py +53 -0
- fitbit_cli-0.1.0/fitbit_cli.egg-info/PKG-INFO +108 -0
- fitbit_cli-0.1.0/fitbit_cli.egg-info/SOURCES.txt +17 -0
- fitbit_cli-0.1.0/fitbit_cli.egg-info/dependency_links.txt +1 -0
- fitbit_cli-0.1.0/fitbit_cli.egg-info/entry_points.txt +2 -0
- fitbit_cli-0.1.0/fitbit_cli.egg-info/requires.txt +2 -0
- fitbit_cli-0.1.0/fitbit_cli.egg-info/top_level.txt +1 -0
- fitbit_cli-0.1.0/pyproject.toml +21 -0
- fitbit_cli-0.1.0/setup.cfg +4 -0
- fitbit_cli-0.1.0/setup.py +56 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fitbit-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Access your Fitbit data at your terminal.
|
|
5
|
+
Home-page: https://github.com/veerendra2/fitbit-cli
|
|
6
|
+
Download-URL: https://github.com/veerendra2/fitbit-cli/archive/0.1.0.tar.gz
|
|
7
|
+
Author: veerendra2
|
|
8
|
+
Author-email: vk.tyk23@simplelogin.com
|
|
9
|
+
License: MIT
|
|
10
|
+
Project-URL: Documentation, https://github.com/veerendra2/fitbit-cli
|
|
11
|
+
Keywords: fitbit,cli,python
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: requests==2.32.3
|
|
29
|
+
Requires-Dist: rich==13.9.4
|
|
30
|
+
|
|
31
|
+
# Fitbit CLI
|
|
32
|
+
|
|
33
|
+
> ⚠️ This is not an official Fitbit CLI
|
|
34
|
+
|
|
35
|
+
Access your Fitbit data directly from your terminal 💻. View 💤 sleep logs, ❤️ heart rate, 🏋️♂️ activity levels, 🩸 SpO2, and more, all presented in a simple, easy-to-read table format!
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img alt="Fitbit logo", width="250" src="./assets/Fitbit_Logo_White_RGB.jpg">
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
## Supported Web APIs
|
|
42
|
+
|
|
43
|
+
> Only `GET` APIs are supported!
|
|
44
|
+
|
|
45
|
+
| API | Status |
|
|
46
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ------ |
|
|
47
|
+
| [User](https://dev.fitbit.com/build/reference/web-api/user/) | ✅ |
|
|
48
|
+
| [Sleep](https://dev.fitbit.com/build/reference/web-api/sleep/) | ✅ |
|
|
49
|
+
| [SpO2](https://dev.fitbit.com/build/reference/web-api/spo2/) | ✅ |
|
|
50
|
+
| [Heart Rate Time Series](https://dev.fitbit.com/build/reference/web-api/heartrate-timeseries/) | ✅ |
|
|
51
|
+
| [Active Zone Minutes (AZM) Time Series](https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/) | ✅ |
|
|
52
|
+
| [Activity](https://dev.fitbit.com/build/reference/web-api/activity/) | 👷 |
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pip install fitbit-cli
|
|
58
|
+
|
|
59
|
+
fitbit-cli -h
|
|
60
|
+
usage: fitbit-cli [-h] [-d | -i] [-s [DATE[,DATE]]] [-o [DATE[,DATE]]] [-e [DATE[,DATE]]] [-a [DATE[,DATE]]] [-u] [-v]
|
|
61
|
+
|
|
62
|
+
Fitbit CLI -- Access your Fitbit data at your terminal.
|
|
63
|
+
|
|
64
|
+
options:
|
|
65
|
+
-h, --help show this help message and exit
|
|
66
|
+
-d, --json-dump Dump all your Fitbit data in json files.
|
|
67
|
+
-i, --init Run interative setup to fetch token.
|
|
68
|
+
-v, --version Show fitbit-cli version
|
|
69
|
+
|
|
70
|
+
APIs:
|
|
71
|
+
Specify date ranges (ISO 8601 format: YYYY-MM-DD) for the following arguments.
|
|
72
|
+
You can provide a single date or a range (start,end). If not provided, defaults to today's date.
|
|
73
|
+
|
|
74
|
+
-s, --sleep [DATE[,DATE]]
|
|
75
|
+
Sleep data
|
|
76
|
+
-o, --spo2 [DATE[,DATE]]
|
|
77
|
+
SpO2 data
|
|
78
|
+
-e, --heart [DATE[,DATE]]
|
|
79
|
+
Heart Rate Time Series
|
|
80
|
+
-a, --active-zone [DATE[,DATE]]
|
|
81
|
+
Active Zone Minutes (AZM) Time Series
|
|
82
|
+
-u, --show-user-profile
|
|
83
|
+
Show user profile data
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Register Fitbit App
|
|
87
|
+
|
|
88
|
+
1. Go to [https://dev.fitbit.com/apps](https://dev.fitbit.com/apps)
|
|
89
|
+
2. Click on "REGISTER AN APP" tab
|
|
90
|
+
3. Follow below example and register an app
|
|
91
|
+
|
|
92
|
+
<p align="left">
|
|
93
|
+
<img alt="Fitbit logo", width="700" src="./assets/fitbit-app-registration.png">
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
## Local Development
|
|
97
|
+
|
|
98
|
+
- [Fitbit Docs](https://dev.fitbit.com/build/reference/web-api/)
|
|
99
|
+
- [OAuth Tutorial](https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/oauth2-tutorial/)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone git@github.com:veerendra2/fitbit-cli.git
|
|
103
|
+
cd fitbit-cli
|
|
104
|
+
|
|
105
|
+
python -m venv venv
|
|
106
|
+
source venv/bin/activate
|
|
107
|
+
python -m pip install -e .
|
|
108
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Fitbit CLI
|
|
2
|
+
|
|
3
|
+
> ⚠️ This is not an official Fitbit CLI
|
|
4
|
+
|
|
5
|
+
Access your Fitbit data directly from your terminal 💻. View 💤 sleep logs, ❤️ heart rate, 🏋️♂️ activity levels, 🩸 SpO2, and more, all presented in a simple, easy-to-read table format!
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img alt="Fitbit logo", width="250" src="./assets/Fitbit_Logo_White_RGB.jpg">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## Supported Web APIs
|
|
12
|
+
|
|
13
|
+
> Only `GET` APIs are supported!
|
|
14
|
+
|
|
15
|
+
| API | Status |
|
|
16
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ------ |
|
|
17
|
+
| [User](https://dev.fitbit.com/build/reference/web-api/user/) | ✅ |
|
|
18
|
+
| [Sleep](https://dev.fitbit.com/build/reference/web-api/sleep/) | ✅ |
|
|
19
|
+
| [SpO2](https://dev.fitbit.com/build/reference/web-api/spo2/) | ✅ |
|
|
20
|
+
| [Heart Rate Time Series](https://dev.fitbit.com/build/reference/web-api/heartrate-timeseries/) | ✅ |
|
|
21
|
+
| [Active Zone Minutes (AZM) Time Series](https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/) | ✅ |
|
|
22
|
+
| [Activity](https://dev.fitbit.com/build/reference/web-api/activity/) | 👷 |
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python -m pip install fitbit-cli
|
|
28
|
+
|
|
29
|
+
fitbit-cli -h
|
|
30
|
+
usage: fitbit-cli [-h] [-d | -i] [-s [DATE[,DATE]]] [-o [DATE[,DATE]]] [-e [DATE[,DATE]]] [-a [DATE[,DATE]]] [-u] [-v]
|
|
31
|
+
|
|
32
|
+
Fitbit CLI -- Access your Fitbit data at your terminal.
|
|
33
|
+
|
|
34
|
+
options:
|
|
35
|
+
-h, --help show this help message and exit
|
|
36
|
+
-d, --json-dump Dump all your Fitbit data in json files.
|
|
37
|
+
-i, --init Run interative setup to fetch token.
|
|
38
|
+
-v, --version Show fitbit-cli version
|
|
39
|
+
|
|
40
|
+
APIs:
|
|
41
|
+
Specify date ranges (ISO 8601 format: YYYY-MM-DD) for the following arguments.
|
|
42
|
+
You can provide a single date or a range (start,end). If not provided, defaults to today's date.
|
|
43
|
+
|
|
44
|
+
-s, --sleep [DATE[,DATE]]
|
|
45
|
+
Sleep data
|
|
46
|
+
-o, --spo2 [DATE[,DATE]]
|
|
47
|
+
SpO2 data
|
|
48
|
+
-e, --heart [DATE[,DATE]]
|
|
49
|
+
Heart Rate Time Series
|
|
50
|
+
-a, --active-zone [DATE[,DATE]]
|
|
51
|
+
Active Zone Minutes (AZM) Time Series
|
|
52
|
+
-u, --show-user-profile
|
|
53
|
+
Show user profile data
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Register Fitbit App
|
|
57
|
+
|
|
58
|
+
1. Go to [https://dev.fitbit.com/apps](https://dev.fitbit.com/apps)
|
|
59
|
+
2. Click on "REGISTER AN APP" tab
|
|
60
|
+
3. Follow below example and register an app
|
|
61
|
+
|
|
62
|
+
<p align="left">
|
|
63
|
+
<img alt="Fitbit logo", width="700" src="./assets/fitbit-app-registration.png">
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
## Local Development
|
|
67
|
+
|
|
68
|
+
- [Fitbit Docs](https://dev.fitbit.com/build/reference/web-api/)
|
|
69
|
+
- [OAuth Tutorial](https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/oauth2-tutorial/)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone git@github.com:veerendra2/fitbit-cli.git
|
|
73
|
+
cd fitbit-cli
|
|
74
|
+
|
|
75
|
+
python -m venv venv
|
|
76
|
+
source venv/bin/activate
|
|
77
|
+
python -m pip install -e .
|
|
78
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
CLI Arguments Parser
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_date_range(date_str):
|
|
13
|
+
"""Date parser"""
|
|
14
|
+
|
|
15
|
+
dates = date_str.split(",")
|
|
16
|
+
start_date = datetime.strptime(dates[0], "%Y-%m-%d").date()
|
|
17
|
+
try:
|
|
18
|
+
end_date = datetime.strptime(dates[1], "%Y-%m-%d").date()
|
|
19
|
+
if start_date > end_date:
|
|
20
|
+
raise ValueError("Start date must not be after end date")
|
|
21
|
+
except IndexError:
|
|
22
|
+
end_date = None
|
|
23
|
+
|
|
24
|
+
return (start_date, end_date)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_arguments():
|
|
28
|
+
"""Argument parser"""
|
|
29
|
+
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
description="Fitbit CLI -- Access your Fitbit data at your terminal.",
|
|
32
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
mutex_group = parser.add_mutually_exclusive_group(required=False)
|
|
36
|
+
|
|
37
|
+
mutex_group.add_argument(
|
|
38
|
+
"-d",
|
|
39
|
+
"--json-dump",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Dump all your Fitbit data in json files.",
|
|
42
|
+
)
|
|
43
|
+
mutex_group.add_argument(
|
|
44
|
+
"-i",
|
|
45
|
+
"--init",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Run interative setup to fetch token.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
group = parser.add_argument_group(
|
|
51
|
+
"APIs",
|
|
52
|
+
"Specify date ranges (ISO 8601 format: YYYY-MM-DD) for the following arguments.\n"
|
|
53
|
+
"You can provide a single date or a range (start,end). If not provided, defaults to today's date.",
|
|
54
|
+
)
|
|
55
|
+
group.add_argument(
|
|
56
|
+
"-s",
|
|
57
|
+
"--sleep",
|
|
58
|
+
type=parse_date_range,
|
|
59
|
+
nargs="?",
|
|
60
|
+
const=(datetime.today().date(), None),
|
|
61
|
+
metavar="DATE[,DATE]",
|
|
62
|
+
help="Sleep data",
|
|
63
|
+
)
|
|
64
|
+
group.add_argument(
|
|
65
|
+
"-o",
|
|
66
|
+
"--spo2",
|
|
67
|
+
type=parse_date_range,
|
|
68
|
+
nargs="?",
|
|
69
|
+
const=(datetime.today().date(), None),
|
|
70
|
+
metavar="DATE[,DATE]",
|
|
71
|
+
help="SpO2 data",
|
|
72
|
+
)
|
|
73
|
+
group.add_argument(
|
|
74
|
+
"-e",
|
|
75
|
+
"--heart",
|
|
76
|
+
type=parse_date_range,
|
|
77
|
+
nargs="?",
|
|
78
|
+
const=(datetime.today().date(), None),
|
|
79
|
+
metavar="DATE[,DATE]",
|
|
80
|
+
help="Heart Rate Time Series",
|
|
81
|
+
)
|
|
82
|
+
group.add_argument(
|
|
83
|
+
"-a",
|
|
84
|
+
"--active-zone",
|
|
85
|
+
type=parse_date_range,
|
|
86
|
+
nargs="?",
|
|
87
|
+
const=(datetime.today().date(), None),
|
|
88
|
+
metavar="DATE[,DATE]",
|
|
89
|
+
help="Active Zone Minutes (AZM) Time Series",
|
|
90
|
+
)
|
|
91
|
+
group.add_argument(
|
|
92
|
+
"-u",
|
|
93
|
+
"--show-user-profile",
|
|
94
|
+
action="store_true",
|
|
95
|
+
help="Show user profile data",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"-v",
|
|
100
|
+
"--version",
|
|
101
|
+
action="version",
|
|
102
|
+
version=f"%(prog)s v{__version__}",
|
|
103
|
+
help="Show fitbit-cli version",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return parser.parse_args()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Exceptions for Fitbit CLI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FitbitInitError(Exception):
|
|
8
|
+
"""Custom exception for Fitbit initial setup"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message):
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.message = message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FitbitAPIError(Exception):
|
|
16
|
+
"""Custom exception for Fitbit API"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message):
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.message = message
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Fitbit API
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .exceptions import FitbitAPIError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FitbitAPI:
|
|
12
|
+
"""Fitbit API"""
|
|
13
|
+
|
|
14
|
+
TOKEN_API = "https://api.fitbit.com/oauth2/token"
|
|
15
|
+
|
|
16
|
+
def __init__(self, client_id, client_secret, access_token, refresh_token):
|
|
17
|
+
self.client_id = client_id
|
|
18
|
+
self.client_secret = client_secret
|
|
19
|
+
self.access_token = access_token
|
|
20
|
+
self.refresh_token = refresh_token
|
|
21
|
+
self.headers = self._create_headers()
|
|
22
|
+
|
|
23
|
+
def _create_headers(self):
|
|
24
|
+
return {
|
|
25
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def refresh_access_token(self):
|
|
30
|
+
"""Refresh token"""
|
|
31
|
+
|
|
32
|
+
payload = {
|
|
33
|
+
"grant_type": "refresh_token",
|
|
34
|
+
"client_id": self.client_id,
|
|
35
|
+
"refresh_token": self.refresh_token,
|
|
36
|
+
}
|
|
37
|
+
headers = {
|
|
38
|
+
"Authorization": f"Basic {self.client_secret}",
|
|
39
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
40
|
+
}
|
|
41
|
+
response = requests.post(
|
|
42
|
+
FitbitAPI.TOKEN_API, data=payload, headers=headers, timeout=5
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if response.status_code == 200:
|
|
46
|
+
tokens = response.json()
|
|
47
|
+
self.access_token = tokens.get("access_token")
|
|
48
|
+
self.refresh_token = tokens.get("refresh_token")
|
|
49
|
+
self.headers = self._create_headers()
|
|
50
|
+
else:
|
|
51
|
+
raise FitbitAPIError("Failed to refresh access token")
|
|
52
|
+
|
|
53
|
+
def make_request(self, method, url, **kwargs):
|
|
54
|
+
"""Make an API request and handle token refresh if needed."""
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = requests.request(
|
|
58
|
+
method, url, headers=self.headers, timeout=5, **kwargs
|
|
59
|
+
)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
except requests.exceptions.HTTPError as e:
|
|
62
|
+
if response.status_code == 401:
|
|
63
|
+
self.refresh_access_token()
|
|
64
|
+
response = requests.request(
|
|
65
|
+
method, url, headers=self.headers, timeout=5, **kwargs
|
|
66
|
+
)
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
else:
|
|
69
|
+
raise FitbitAPIError(f"HTTP error occurred: {response.json()}") from e
|
|
70
|
+
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
def get_user_profile(self):
|
|
74
|
+
"""Get Profile"""
|
|
75
|
+
|
|
76
|
+
url = "https://api.fitbit.com/1/user/-/profile.json"
|
|
77
|
+
response = self.make_request("GET", url)
|
|
78
|
+
return response.json()
|
|
79
|
+
|
|
80
|
+
def get_sleep_log(self, start_date, end_date=None):
|
|
81
|
+
"""Get Sleep Logs by Date Range and Date"""
|
|
82
|
+
|
|
83
|
+
date_range = f"{start_date}/{end_date}" if end_date else start_date
|
|
84
|
+
url = f"https://api.fitbit.com/1.2/user/-/sleep/date/{date_range}.json"
|
|
85
|
+
response = self.make_request("GET", url)
|
|
86
|
+
return response.json()
|
|
87
|
+
|
|
88
|
+
def get_heart_rate_time_series(self, start_date, end_date=None):
|
|
89
|
+
"""Get Heart Rate Time Series by Date Range and Date"""
|
|
90
|
+
|
|
91
|
+
date_range = f"{start_date}/{end_date}" if end_date else f"{start_date}/1d"
|
|
92
|
+
url = f"https://api.fitbit.com/1/user/-/activities/heart/date/{date_range}.json"
|
|
93
|
+
response = self.make_request("GET", url)
|
|
94
|
+
return response.json()
|
|
95
|
+
|
|
96
|
+
def get_spo2_summary(self, start_date, end_date=None):
|
|
97
|
+
"""Get SpO2 Summary by Interval and Date"""
|
|
98
|
+
|
|
99
|
+
date_range = f"{start_date}/{end_date}" if end_date else start_date
|
|
100
|
+
url = f"https://api.fitbit.com/1/user/-/spo2/date/{date_range}.json"
|
|
101
|
+
response = self.make_request("GET", url)
|
|
102
|
+
return response.json()
|
|
103
|
+
|
|
104
|
+
def get_spo2_intraday(self, start_date, end_date=None):
|
|
105
|
+
"""Get SpO2 Intraday by Interval and Date"""
|
|
106
|
+
|
|
107
|
+
date_range = f"{start_date}/{end_date}" if end_date else start_date
|
|
108
|
+
url = f"https://api.fitbit.com/1/user/-/spo2/date/{date_range}/all.json"
|
|
109
|
+
response = self.make_request("GET", url)
|
|
110
|
+
return response.json()
|
|
111
|
+
|
|
112
|
+
def get_azm_time_series(self, start_date, end_date=None):
|
|
113
|
+
"""Get AZM Time Series by Interval and Data"""
|
|
114
|
+
|
|
115
|
+
date_range = f"{start_date}/{end_date}" if end_date else f"{start_date}/1d"
|
|
116
|
+
url = f"https://api.fitbit.com/1/user/-/activities/active-zone-minutes/date/{date_range}.json"
|
|
117
|
+
response = self.make_request("GET", url)
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
def get_azm_intraday(self, start_date, end_date=None):
|
|
121
|
+
"""Get AZM Intraday by Interval and Data"""
|
|
122
|
+
|
|
123
|
+
date_range = f"{start_date}/{end_date}" if end_date else f"{start_date}/1d"
|
|
124
|
+
url = f"https://api.fitbit.com/1/user/-/activities/active-zone-minutes/date/{date_range}/1min.json"
|
|
125
|
+
response = self.make_request("GET", url)
|
|
126
|
+
return response.json()
|
|
127
|
+
|
|
128
|
+
def get_breathing_rate_summary(self, start_date, end_date=None):
|
|
129
|
+
"""Get Breathing Rate Summary by Interval and Data"""
|
|
130
|
+
|
|
131
|
+
date_range = f"{start_date}/{end_date}" if end_date else start_date
|
|
132
|
+
url = f"https://api.fitbit.com/1/user/-/br/date/{date_range}.json"
|
|
133
|
+
response = self.make_request("GET", url)
|
|
134
|
+
return response.json()
|
|
135
|
+
|
|
136
|
+
def get_breathing_rate_intraday(self, start_date, end_date=None):
|
|
137
|
+
"""Get Breathing Rate Intraday by Interval and Data"""
|
|
138
|
+
|
|
139
|
+
date_range = f"{start_date}/{end_date}" if end_date else start_date
|
|
140
|
+
url = f"https://api.fitbit.com/1/user/-/br/date/{date_range}/all.json"
|
|
141
|
+
response = self.make_request("GET", url)
|
|
142
|
+
return response.json()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Fitbit initial setup
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import getpass
|
|
7
|
+
import json
|
|
8
|
+
import secrets
|
|
9
|
+
import string
|
|
10
|
+
import threading
|
|
11
|
+
import webbrowser
|
|
12
|
+
from base64 import b64encode, urlsafe_b64encode
|
|
13
|
+
from hashlib import sha256
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from urllib.parse import parse_qs, urlparse
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
from rich.prompt import Prompt
|
|
20
|
+
|
|
21
|
+
from .exceptions import FitbitInitError
|
|
22
|
+
from .formatter import CONSOLE
|
|
23
|
+
|
|
24
|
+
BASE_URL = "https://www.fitbit.com/oauth2/authorize"
|
|
25
|
+
FITBIT_TOKEN_PATH = f"{Path.home()}/.fitbit/token.json"
|
|
26
|
+
SCOPE = [
|
|
27
|
+
"activity",
|
|
28
|
+
"cardio_fitness",
|
|
29
|
+
"electrocardiogram",
|
|
30
|
+
"heartrate",
|
|
31
|
+
"irregular_rhythm_notifications",
|
|
32
|
+
"location",
|
|
33
|
+
"nutrition",
|
|
34
|
+
"oxygen_saturation",
|
|
35
|
+
"profile",
|
|
36
|
+
"respiratory_rate",
|
|
37
|
+
"settings",
|
|
38
|
+
"sleep",
|
|
39
|
+
"social",
|
|
40
|
+
"temperature",
|
|
41
|
+
"weight",
|
|
42
|
+
]
|
|
43
|
+
TOKEN_URL = "https://api.fitbit.com/oauth2/token"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RequestHandler(BaseHTTPRequestHandler):
|
|
47
|
+
"""Simple HTTP request handler"""
|
|
48
|
+
|
|
49
|
+
code = None
|
|
50
|
+
|
|
51
|
+
def do_GET(self): # pylint: disable=C0103
|
|
52
|
+
"""Handle GET request"""
|
|
53
|
+
parsed_url = urlparse(self.path)
|
|
54
|
+
query_params = parse_qs(parsed_url.query)
|
|
55
|
+
RequestHandler.code = query_params.get("code", [""])[0]
|
|
56
|
+
|
|
57
|
+
self.send_response(200)
|
|
58
|
+
self.send_header("Content-type", "text/html")
|
|
59
|
+
self.end_headers()
|
|
60
|
+
self.wfile.write(b"You can close this window now")
|
|
61
|
+
|
|
62
|
+
threading.Thread(target=self.server.shutdown).start()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def start_server():
|
|
66
|
+
"""Start simple HTTP server to catch token"""
|
|
67
|
+
|
|
68
|
+
with HTTPServer(("127.0.0.1", 8080), RequestHandler) as httpd:
|
|
69
|
+
CONSOLE.print(":computer: Serving on http://127.0.0.1:8080")
|
|
70
|
+
httpd.serve_forever()
|
|
71
|
+
return RequestHandler.code
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fitbit_init_setup():
|
|
75
|
+
"""Fitbit initial setup"""
|
|
76
|
+
|
|
77
|
+
# --------- Generate code challenge using random strings ---------
|
|
78
|
+
code_verifier = "".join(
|
|
79
|
+
secrets.choice(string.ascii_letters + string.digits) for _ in range(128)
|
|
80
|
+
)
|
|
81
|
+
code_challenge = (
|
|
82
|
+
urlsafe_b64encode(sha256(code_verifier.encode("utf-8")).digest())
|
|
83
|
+
.rstrip(b"=")
|
|
84
|
+
.decode("utf-8")
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# --------- Get client ID and constructs authorization URL ---------
|
|
88
|
+
client_id = Prompt.ask(":bust_in_silhouette: Enter your Client ID")
|
|
89
|
+
assert client_id, "Invalid Client ID"
|
|
90
|
+
|
|
91
|
+
authorization_url = (
|
|
92
|
+
f"{BASE_URL}?client_id={client_id}&response_type=code"
|
|
93
|
+
f"&code_challenge={code_challenge}&code_challenge_method=S256"
|
|
94
|
+
f"&scope={'+'.join(SCOPE)}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# --------- Get authorization code ---------
|
|
98
|
+
CONSOLE.print(
|
|
99
|
+
f":earth_asia: Opening below URL in browser to authorize the app \n\n{authorization_url}\n"
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
browser_status = webbrowser.open(authorization_url)
|
|
103
|
+
if browser_status:
|
|
104
|
+
CONSOLE.print(
|
|
105
|
+
":satellite: Waiting for authorization... "
|
|
106
|
+
+ "(Check your browser or press 'Ctrl+C', authrize the app by opening the"
|
|
107
|
+
+ " above URL in your browser and past the redirect URL manually.)\n"
|
|
108
|
+
)
|
|
109
|
+
authorization_code = start_server()
|
|
110
|
+
else:
|
|
111
|
+
raise FitbitInitError("Failed to open the URL in browser")
|
|
112
|
+
except (KeyboardInterrupt, FitbitInitError):
|
|
113
|
+
CONSOLE.print("\n:unamused: Error while opening the URL...")
|
|
114
|
+
CONSOLE.print(
|
|
115
|
+
":neutral_face: Authrize the app by opening the above URL in your"
|
|
116
|
+
+ "browser and past the redirect URL"
|
|
117
|
+
)
|
|
118
|
+
redirect_url = Prompt.ask(":clipboard: Paste the redirect URL: ")
|
|
119
|
+
assert redirect_url, "Invalid URL"
|
|
120
|
+
|
|
121
|
+
authorization_code = parse_qs(urlparse(redirect_url).query).get("code", [None])[
|
|
122
|
+
0
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# --------- Get access and refresh tokens ---------
|
|
126
|
+
client_secret = getpass.getpass("\n🔑 Enter your Client Secret: ")
|
|
127
|
+
assert client_secret, "Invalid Client Secret"
|
|
128
|
+
|
|
129
|
+
encoded_auth = b64encode(f"{client_id}:{client_secret}".encode()).decode()
|
|
130
|
+
response = requests.post(
|
|
131
|
+
TOKEN_URL,
|
|
132
|
+
headers={
|
|
133
|
+
"Authorization": f"Basic {encoded_auth}",
|
|
134
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
135
|
+
},
|
|
136
|
+
data={
|
|
137
|
+
"client_id": client_id,
|
|
138
|
+
"grant_type": "authorization_code",
|
|
139
|
+
"code": authorization_code,
|
|
140
|
+
"code_verifier": code_verifier,
|
|
141
|
+
},
|
|
142
|
+
timeout=5,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if response.status_code == 200:
|
|
146
|
+
response_json = response.json()
|
|
147
|
+
Path(FITBIT_TOKEN_PATH).parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
token_content = {
|
|
149
|
+
"access_token": response_json.get("access_token", ""),
|
|
150
|
+
"refresh_token": response_json.get("refresh_token", ""),
|
|
151
|
+
"client_id": client_id,
|
|
152
|
+
"secret": encoded_auth,
|
|
153
|
+
}
|
|
154
|
+
with open(FITBIT_TOKEN_PATH, "w", encoding="utf-8") as f:
|
|
155
|
+
json.dump(token_content, f)
|
|
156
|
+
|
|
157
|
+
CONSOLE.print(
|
|
158
|
+
f":floppy_disk: Saving fitbit token in {FITBIT_TOKEN_PATH}",
|
|
159
|
+
style="bold green",
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
CONSOLE.print(
|
|
163
|
+
f":unamused: Failed to get tokens: {response.json()['errors'][0]['errorType']}",
|
|
164
|
+
style="bold red",
|
|
165
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Json Data Formatter
|
|
4
|
+
"""
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
CONSOLE = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def display_user_profile(user_data):
|
|
12
|
+
"""User data formatter"""
|
|
13
|
+
|
|
14
|
+
table = Table(
|
|
15
|
+
title=f"Hello, {user_data['user']['displayName']} :wave:", show_header=False
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
table.add_column("")
|
|
19
|
+
table.add_column("")
|
|
20
|
+
|
|
21
|
+
table.add_row(":bust_in_silhouette: First Name", user_data["user"]["firstName"])
|
|
22
|
+
table.add_row(":family: Last Name", user_data["user"]["lastName"])
|
|
23
|
+
table.add_row(":birthday: Date Of Birth", user_data["user"]["dateOfBirth"])
|
|
24
|
+
table.add_row(":hourglass_flowing_sand: Age", str(user_data["user"]["age"]))
|
|
25
|
+
table.add_row(":restroom: Gender", user_data["user"]["gender"])
|
|
26
|
+
table.add_row(":straight_ruler: Height", f"{user_data['user']['height']:.1f}")
|
|
27
|
+
table.add_row(":weight_lifter: Weight", f"{user_data['user']['weight']:.1f}")
|
|
28
|
+
table.add_row(
|
|
29
|
+
":footprints: Average Daily Steps", str(user_data["user"]["averageDailySteps"])
|
|
30
|
+
)
|
|
31
|
+
table.add_row(":calendar: Member Since", user_data["user"]["memberSince"])
|
|
32
|
+
table.add_row(":clock1: Time Zone", user_data["user"]["timezone"])
|
|
33
|
+
|
|
34
|
+
CONSOLE.print(table)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def display_sleep(sleep_data):
|
|
38
|
+
"""Sleep data formatter"""
|
|
39
|
+
|
|
40
|
+
table = Table(title="Sleep Data Summary :sleeping:", show_header=True)
|
|
41
|
+
|
|
42
|
+
table.add_column("Date :calendar:")
|
|
43
|
+
table.add_column("Deep Sleep :bed:")
|
|
44
|
+
table.add_column("Light Sleep :zzz:")
|
|
45
|
+
table.add_column("REM Sleep :crescent_moon:")
|
|
46
|
+
table.add_column("Wake Time :alarm_clock:")
|
|
47
|
+
table.add_column("Efficiency :100:")
|
|
48
|
+
|
|
49
|
+
for sleep in sleep_data["sleep"]:
|
|
50
|
+
table.add_row(
|
|
51
|
+
sleep["dateOfSleep"],
|
|
52
|
+
f"{sleep['levels']['summary'].get('deep', {}).get('minutes', 'N/A')} min",
|
|
53
|
+
f"{sleep['levels']['summary'].get('light', {}).get('minutes', 'N/A')} min",
|
|
54
|
+
f"{sleep['levels']['summary'].get('rem', {}).get('minutes', 'N/A')} min",
|
|
55
|
+
f"{sleep['levels']['summary'].get('wake', {}).get('minutes', 'N/A')} min",
|
|
56
|
+
f"{sleep['efficiency']}%",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
CONSOLE.print(table)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def display_spo2(spo2_data):
|
|
63
|
+
"""SpO2 data formatter"""
|
|
64
|
+
|
|
65
|
+
table = Table(title="SpO2 Data Summary :heart:", show_header=True)
|
|
66
|
+
|
|
67
|
+
table.add_column("Date :calendar:")
|
|
68
|
+
table.add_column("Minimum :red_circle:")
|
|
69
|
+
table.add_column("Average :blue_circle:")
|
|
70
|
+
table.add_column("Maximum :green_circle:")
|
|
71
|
+
|
|
72
|
+
if isinstance(spo2_data, dict):
|
|
73
|
+
spo2_data = [spo2_data]
|
|
74
|
+
|
|
75
|
+
for spo2 in spo2_data:
|
|
76
|
+
table.add_row(
|
|
77
|
+
spo2["dateTime"],
|
|
78
|
+
str(spo2["value"].get("min", "N/A")),
|
|
79
|
+
str(spo2["value"].get("avg", "N/A")),
|
|
80
|
+
str(spo2["value"].get("max", "N/A")),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
CONSOLE.print(table)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def display_heart_data(heart_data):
|
|
87
|
+
"""Heart data formatter"""
|
|
88
|
+
|
|
89
|
+
table = Table(title="Heart Rate Time Series :heart:", show_header=True)
|
|
90
|
+
|
|
91
|
+
table.add_column("Date :calendar:")
|
|
92
|
+
table.add_column("Resting Heart Rate :heartpulse:")
|
|
93
|
+
table.add_column("Heart Rate Zones :dart:")
|
|
94
|
+
|
|
95
|
+
for activity in heart_data.get("activities-heart", []):
|
|
96
|
+
date = activity.get("dateTime", "N/A")
|
|
97
|
+
value = activity.get("value", {})
|
|
98
|
+
resting_heart_rate = value.get("restingHeartRate", "N/A")
|
|
99
|
+
|
|
100
|
+
zones_table = Table(show_header=True, header_style="bold magenta")
|
|
101
|
+
zones_table.add_column("Zone :dart:")
|
|
102
|
+
zones_table.add_column("Min :arrow_down:")
|
|
103
|
+
zones_table.add_column("Max :arrow_up:")
|
|
104
|
+
zones_table.add_column("Minutes :hourglass:")
|
|
105
|
+
zones_table.add_column("Calories Out :fire:")
|
|
106
|
+
|
|
107
|
+
for zone in value.get("heartRateZones", []):
|
|
108
|
+
zones_table.add_row(
|
|
109
|
+
zone.get("name", "N/A"),
|
|
110
|
+
str(zone.get("min", "N/A")),
|
|
111
|
+
str(zone.get("max", "N/A")),
|
|
112
|
+
str(zone.get("minutes", "N/A")),
|
|
113
|
+
(
|
|
114
|
+
f"{zone.get('caloriesOut', 'N/A'):.2f}"
|
|
115
|
+
if isinstance(zone.get("caloriesOut"), (int, float))
|
|
116
|
+
else "N/A"
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
table.add_row(date, str(resting_heart_rate), zones_table)
|
|
121
|
+
|
|
122
|
+
CONSOLE.print(table)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def display_azm_time_series(azm_data):
|
|
126
|
+
"""AZM Time Series data formatter"""
|
|
127
|
+
|
|
128
|
+
table = Table(title="AZM Time Series :runner:", show_header=True)
|
|
129
|
+
|
|
130
|
+
table.add_column("Date :calendar:")
|
|
131
|
+
table.add_column("Active Zone Minutes :stopwatch:")
|
|
132
|
+
table.add_column("Fat Burn :fire:")
|
|
133
|
+
table.add_column("Cardio :heart:")
|
|
134
|
+
table.add_column("Peak :mountain:")
|
|
135
|
+
|
|
136
|
+
for activity in azm_data.get("activities-active-zone-minutes", []):
|
|
137
|
+
date = activity.get("dateTime", "N/A")
|
|
138
|
+
value = activity.get("value", {})
|
|
139
|
+
table.add_row(
|
|
140
|
+
date,
|
|
141
|
+
str(value.get("activeZoneMinutes", "N/A")),
|
|
142
|
+
str(value.get("fatBurnActiveZoneMinutes", "N/A")),
|
|
143
|
+
str(value.get("cardioActiveZoneMinutes", "N/A")),
|
|
144
|
+
str(value.get("peakActiveZoneMinutes", "N/A")),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
CONSOLE.print(table)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Main Module
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import formatter
|
|
10
|
+
from .cli import parse_arguments
|
|
11
|
+
from .fitbit_api import FitbitAPI
|
|
12
|
+
from .fitbit_init import FITBIT_TOKEN_PATH, fitbit_init_setup
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""Main function"""
|
|
17
|
+
|
|
18
|
+
args = parse_arguments()
|
|
19
|
+
|
|
20
|
+
if args.init:
|
|
21
|
+
fitbit_init_setup()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(FITBIT_TOKEN_PATH, encoding="utf-8") as f:
|
|
25
|
+
credentials = json.load(f)
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
formatter.CONSOLE.print(
|
|
28
|
+
":grimacing: Fitbit token file not found. "
|
|
29
|
+
"If this is your first time running the CLI, use '--init' argument to set up the token.",
|
|
30
|
+
style="bold red",
|
|
31
|
+
)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
fitbit = FitbitAPI(
|
|
35
|
+
client_id=credentials["client_id"],
|
|
36
|
+
client_secret=credentials["secret"],
|
|
37
|
+
access_token=credentials["access_token"],
|
|
38
|
+
refresh_token=credentials["refresh_token"],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
with formatter.CONSOLE.status("[bold green]Fetching data...") as _:
|
|
42
|
+
if args.show_user_profile:
|
|
43
|
+
formatter.display_user_profile(fitbit.get_user_profile())
|
|
44
|
+
if args.sleep:
|
|
45
|
+
formatter.display_sleep(fitbit.get_sleep_log(*args.sleep))
|
|
46
|
+
if args.spo2:
|
|
47
|
+
formatter.display_spo2(fitbit.get_spo2_summary(*args.spo2))
|
|
48
|
+
if args.heart:
|
|
49
|
+
formatter.display_heart_data(fitbit.get_heart_rate_time_series(*args.heart))
|
|
50
|
+
if args.active_zone:
|
|
51
|
+
formatter.display_azm_time_series(
|
|
52
|
+
fitbit.get_azm_time_series(*args.active_zone)
|
|
53
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fitbit-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Access your Fitbit data at your terminal.
|
|
5
|
+
Home-page: https://github.com/veerendra2/fitbit-cli
|
|
6
|
+
Download-URL: https://github.com/veerendra2/fitbit-cli/archive/0.1.0.tar.gz
|
|
7
|
+
Author: veerendra2
|
|
8
|
+
Author-email: vk.tyk23@simplelogin.com
|
|
9
|
+
License: MIT
|
|
10
|
+
Project-URL: Documentation, https://github.com/veerendra2/fitbit-cli
|
|
11
|
+
Keywords: fitbit,cli,python
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: requests==2.32.3
|
|
29
|
+
Requires-Dist: rich==13.9.4
|
|
30
|
+
|
|
31
|
+
# Fitbit CLI
|
|
32
|
+
|
|
33
|
+
> ⚠️ This is not an official Fitbit CLI
|
|
34
|
+
|
|
35
|
+
Access your Fitbit data directly from your terminal 💻. View 💤 sleep logs, ❤️ heart rate, 🏋️♂️ activity levels, 🩸 SpO2, and more, all presented in a simple, easy-to-read table format!
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img alt="Fitbit logo", width="250" src="./assets/Fitbit_Logo_White_RGB.jpg">
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
## Supported Web APIs
|
|
42
|
+
|
|
43
|
+
> Only `GET` APIs are supported!
|
|
44
|
+
|
|
45
|
+
| API | Status |
|
|
46
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ------ |
|
|
47
|
+
| [User](https://dev.fitbit.com/build/reference/web-api/user/) | ✅ |
|
|
48
|
+
| [Sleep](https://dev.fitbit.com/build/reference/web-api/sleep/) | ✅ |
|
|
49
|
+
| [SpO2](https://dev.fitbit.com/build/reference/web-api/spo2/) | ✅ |
|
|
50
|
+
| [Heart Rate Time Series](https://dev.fitbit.com/build/reference/web-api/heartrate-timeseries/) | ✅ |
|
|
51
|
+
| [Active Zone Minutes (AZM) Time Series](https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/) | ✅ |
|
|
52
|
+
| [Activity](https://dev.fitbit.com/build/reference/web-api/activity/) | 👷 |
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pip install fitbit-cli
|
|
58
|
+
|
|
59
|
+
fitbit-cli -h
|
|
60
|
+
usage: fitbit-cli [-h] [-d | -i] [-s [DATE[,DATE]]] [-o [DATE[,DATE]]] [-e [DATE[,DATE]]] [-a [DATE[,DATE]]] [-u] [-v]
|
|
61
|
+
|
|
62
|
+
Fitbit CLI -- Access your Fitbit data at your terminal.
|
|
63
|
+
|
|
64
|
+
options:
|
|
65
|
+
-h, --help show this help message and exit
|
|
66
|
+
-d, --json-dump Dump all your Fitbit data in json files.
|
|
67
|
+
-i, --init Run interative setup to fetch token.
|
|
68
|
+
-v, --version Show fitbit-cli version
|
|
69
|
+
|
|
70
|
+
APIs:
|
|
71
|
+
Specify date ranges (ISO 8601 format: YYYY-MM-DD) for the following arguments.
|
|
72
|
+
You can provide a single date or a range (start,end). If not provided, defaults to today's date.
|
|
73
|
+
|
|
74
|
+
-s, --sleep [DATE[,DATE]]
|
|
75
|
+
Sleep data
|
|
76
|
+
-o, --spo2 [DATE[,DATE]]
|
|
77
|
+
SpO2 data
|
|
78
|
+
-e, --heart [DATE[,DATE]]
|
|
79
|
+
Heart Rate Time Series
|
|
80
|
+
-a, --active-zone [DATE[,DATE]]
|
|
81
|
+
Active Zone Minutes (AZM) Time Series
|
|
82
|
+
-u, --show-user-profile
|
|
83
|
+
Show user profile data
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Register Fitbit App
|
|
87
|
+
|
|
88
|
+
1. Go to [https://dev.fitbit.com/apps](https://dev.fitbit.com/apps)
|
|
89
|
+
2. Click on "REGISTER AN APP" tab
|
|
90
|
+
3. Follow below example and register an app
|
|
91
|
+
|
|
92
|
+
<p align="left">
|
|
93
|
+
<img alt="Fitbit logo", width="700" src="./assets/fitbit-app-registration.png">
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
## Local Development
|
|
97
|
+
|
|
98
|
+
- [Fitbit Docs](https://dev.fitbit.com/build/reference/web-api/)
|
|
99
|
+
- [OAuth Tutorial](https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/oauth2-tutorial/)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone git@github.com:veerendra2/fitbit-cli.git
|
|
103
|
+
cd fitbit-cli
|
|
104
|
+
|
|
105
|
+
python -m venv venv
|
|
106
|
+
source venv/bin/activate
|
|
107
|
+
python -m pip install -e .
|
|
108
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
fitbit_cli/__init__.py
|
|
6
|
+
fitbit_cli/cli.py
|
|
7
|
+
fitbit_cli/exceptions.py
|
|
8
|
+
fitbit_cli/fitbit_api.py
|
|
9
|
+
fitbit_cli/fitbit_init.py
|
|
10
|
+
fitbit_cli/formatter.py
|
|
11
|
+
fitbit_cli/main.py
|
|
12
|
+
fitbit_cli.egg-info/PKG-INFO
|
|
13
|
+
fitbit_cli.egg-info/SOURCES.txt
|
|
14
|
+
fitbit_cli.egg-info/dependency_links.txt
|
|
15
|
+
fitbit_cli.egg-info/entry_points.txt
|
|
16
|
+
fitbit_cli.egg-info/requires.txt
|
|
17
|
+
fitbit_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fitbit_cli
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.black]
|
|
6
|
+
line-length = 88
|
|
7
|
+
|
|
8
|
+
[tool.pylint.messages_control]
|
|
9
|
+
disable = ["E0401"]
|
|
10
|
+
|
|
11
|
+
[tool.isort]
|
|
12
|
+
profile = "black"
|
|
13
|
+
|
|
14
|
+
[tool.mypy]
|
|
15
|
+
ignore_missing_imports = true
|
|
16
|
+
|
|
17
|
+
[tool.pylint.report]
|
|
18
|
+
output-format = "colorized"
|
|
19
|
+
|
|
20
|
+
[tool.pylint.reporter]
|
|
21
|
+
max-line-length = 120
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
setup.py
|
|
4
|
+
"""
|
|
5
|
+
# pylint: disable=C0301
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from setuptools import find_packages, setup
|
|
10
|
+
|
|
11
|
+
with open("fitbit_cli/__init__.py", encoding="utf-8") as file:
|
|
12
|
+
REGEX_VERSION = r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]'
|
|
13
|
+
version = re.search(REGEX_VERSION, file.read(), re.MULTILINE).group(1) # type: ignore[union-attr]
|
|
14
|
+
|
|
15
|
+
with open("README.md", encoding="utf-8") as file:
|
|
16
|
+
readme = file.read()
|
|
17
|
+
|
|
18
|
+
setup(
|
|
19
|
+
name="fitbit-cli",
|
|
20
|
+
version=version,
|
|
21
|
+
packages=find_packages(),
|
|
22
|
+
description="Access your Fitbit data at your terminal.",
|
|
23
|
+
long_description=readme,
|
|
24
|
+
long_description_content_type="text/markdown",
|
|
25
|
+
author="veerendra2",
|
|
26
|
+
author_email="vk.tyk23@simplelogin.com",
|
|
27
|
+
url="https://github.com/veerendra2/fitbit-cli",
|
|
28
|
+
download_url=f"https://github.com/veerendra2/fitbit-cli/archive/{version}.tar.gz",
|
|
29
|
+
project_urls={
|
|
30
|
+
"Documentation": "https://github.com/veerendra2/fitbit-cli",
|
|
31
|
+
},
|
|
32
|
+
keywords=["fitbit", "cli", "python"],
|
|
33
|
+
license="MIT",
|
|
34
|
+
classifiers=[
|
|
35
|
+
"Development Status :: 4 - Beta",
|
|
36
|
+
"Intended Audience :: End Users/Desktop",
|
|
37
|
+
"License :: OSI Approved :: Apache Software License",
|
|
38
|
+
"Natural Language :: English",
|
|
39
|
+
"Operating System :: POSIX :: Linux",
|
|
40
|
+
"Programming Language :: Python",
|
|
41
|
+
"Programming Language :: Python :: 3",
|
|
42
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
43
|
+
"Programming Language :: Python :: 3.9",
|
|
44
|
+
"Programming Language :: Python :: 3.10",
|
|
45
|
+
"Programming Language :: Python :: 3.11",
|
|
46
|
+
"Programming Language :: Python :: 3.12",
|
|
47
|
+
"Programming Language :: Python :: 3.13",
|
|
48
|
+
"Topic :: Utilities",
|
|
49
|
+
],
|
|
50
|
+
install_requires=[
|
|
51
|
+
"requests==2.32.3",
|
|
52
|
+
"rich==13.9.4",
|
|
53
|
+
],
|
|
54
|
+
python_requires=">=3.9",
|
|
55
|
+
entry_points={"console_scripts": ["fitbit-cli = fitbit_cli.main:main"]},
|
|
56
|
+
)
|