fitbit-cli 1.5.2__tar.gz → 1.6.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-1.5.2/fitbit_cli.egg-info → fitbit_cli-1.6.0}/PKG-INFO +14 -10
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/README.md +7 -2
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/__init__.py +1 -1
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/cli.py +21 -1
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/formatter.py +165 -17
- fitbit_cli-1.6.0/fitbit_cli/main.py +35 -0
- fitbit_cli-1.6.0/fitbit_cli/output.py +137 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0/fitbit_cli.egg-info}/PKG-INFO +14 -10
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/SOURCES.txt +1 -0
- fitbit_cli-1.6.0/fitbit_cli.egg-info/requires.txt +2 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/setup.py +5 -6
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/tests/cli_test.py +38 -1
- fitbit_cli-1.5.2/fitbit_cli/main.py +0 -76
- fitbit_cli-1.5.2/fitbit_cli.egg-info/requires.txt +0 -2
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/LICENSE +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/MANIFEST.in +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/exceptions.py +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/fitbit_api.py +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/fitbit_setup.py +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/dependency_links.txt +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/entry_points.txt +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/top_level.txt +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/pyproject.toml +0 -0
- {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fitbit-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Access your Fitbit data at your terminal.
|
|
5
5
|
Home-page: https://github.com/veerendra2/fitbit-cli
|
|
6
|
-
Download-URL: https://github.com/veerendra2/fitbit-cli/archive/1.
|
|
6
|
+
Download-URL: https://github.com/veerendra2/fitbit-cli/archive/1.6.0.tar.gz
|
|
7
7
|
Author: veerendra2
|
|
8
8
|
Author-email: vk.tyk23@simplelogin.com
|
|
9
9
|
License: MIT
|
|
@@ -11,22 +11,21 @@ Project-URL: Documentation, https://github.com/veerendra2/fitbit-cli
|
|
|
11
11
|
Keywords: fitbit,fitbit-api,cli,python
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
-
Classifier: License :: OSI Approved ::
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Natural Language :: English
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.
|
|
22
|
-
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
22
|
Classifier: Programming Language :: Python
|
|
24
23
|
Classifier: Topic :: Utilities
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.12
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist: requests==2.
|
|
29
|
-
Requires-Dist: rich==14.3.
|
|
27
|
+
Requires-Dist: requests==2.33.0
|
|
28
|
+
Requires-Dist: rich==14.3.3
|
|
30
29
|
Dynamic: author
|
|
31
30
|
Dynamic: author-email
|
|
32
31
|
Dynamic: classifier
|
|
@@ -49,10 +48,12 @@ Dynamic: summary
|
|
|
49
48
|
](https://pypi.org/project/fitbit-cli/) [
|
|
50
49
|
](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
|
|
51
50
|
|
|
52
|
-
>
|
|
51
|
+
> _This is not an official Fitbit CLI_
|
|
53
52
|
|
|
54
53
|
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!
|
|
55
54
|
|
|
55
|
+
> **AI agent-friendly** 🤖 — since v1.6.0, use `--json` for minimized, token-efficient JSON output or `--raw-json` for the full Fitbit API response. No spinners, pure JSON.
|
|
56
|
+
|
|
56
57
|
<p align="center">
|
|
57
58
|
<img alt="Fitbit logo", width="350" src="https://raw.githubusercontent.com/veerendra2/fitbit-cli/refs/heads/main/assets/Fitbit_Logo_White_RGB.jpg">
|
|
58
59
|
</p>
|
|
@@ -86,13 +87,16 @@ python -m pip install fitbit-cli
|
|
|
86
87
|
|
|
87
88
|
```bash
|
|
88
89
|
fitbit-cli -h
|
|
89
|
-
usage: fitbit-cli [-h] [-i] [-
|
|
90
|
+
usage: fitbit-cli [-h] [-i] [-j] [-r] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]]
|
|
91
|
+
[-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
|
|
90
92
|
|
|
91
93
|
Fitbit CLI -- Access your Fitbit data at your terminal.
|
|
92
94
|
|
|
93
95
|
options:
|
|
94
96
|
-h, --help show this help message and exit
|
|
95
97
|
-i, --init-auth Initialize Fitbit iterative authentication setup
|
|
98
|
+
-j, --json Output table data as JSON.
|
|
99
|
+
-r, --raw-json Output raw JSON from the Fitbit API.
|
|
96
100
|
-v, --version Show fitbit-cli version
|
|
97
101
|
|
|
98
102
|
APIs:
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
](https://pypi.org/project/fitbit-cli/) [
|
|
6
6
|
](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
|
|
7
7
|
|
|
8
|
-
>
|
|
8
|
+
> _This is not an official Fitbit CLI_
|
|
9
9
|
|
|
10
10
|
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!
|
|
11
11
|
|
|
12
|
+
> **AI agent-friendly** 🤖 — since v1.6.0, use `--json` for minimized, token-efficient JSON output or `--raw-json` for the full Fitbit API response. No spinners, pure JSON.
|
|
13
|
+
|
|
12
14
|
<p align="center">
|
|
13
15
|
<img alt="Fitbit logo", width="350" src="https://raw.githubusercontent.com/veerendra2/fitbit-cli/refs/heads/main/assets/Fitbit_Logo_White_RGB.jpg">
|
|
14
16
|
</p>
|
|
@@ -42,13 +44,16 @@ python -m pip install fitbit-cli
|
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
46
|
fitbit-cli -h
|
|
45
|
-
usage: fitbit-cli [-h] [-i] [-
|
|
47
|
+
usage: fitbit-cli [-h] [-i] [-j] [-r] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]]
|
|
48
|
+
[-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
|
|
46
49
|
|
|
47
50
|
Fitbit CLI -- Access your Fitbit data at your terminal.
|
|
48
51
|
|
|
49
52
|
options:
|
|
50
53
|
-h, --help show this help message and exit
|
|
51
54
|
-i, --init-auth Initialize Fitbit iterative authentication setup
|
|
55
|
+
-j, --json Output table data as JSON.
|
|
56
|
+
-r, --raw-json Output raw JSON from the Fitbit API.
|
|
52
57
|
-v, --version Show fitbit-cli version
|
|
53
58
|
|
|
54
59
|
APIs:
|
|
@@ -72,6 +72,20 @@ def parse_arguments():
|
|
|
72
72
|
help="Initialize Fitbit iterative authentication setup",
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"-j",
|
|
77
|
+
"--json",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Output table data as JSON.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"-r",
|
|
84
|
+
"--raw-json",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Output raw JSON from the Fitbit API.",
|
|
87
|
+
)
|
|
88
|
+
|
|
75
89
|
group = parser.add_argument_group(
|
|
76
90
|
"APIs",
|
|
77
91
|
"Specify a date, date range (YYYY-MM-DD[,YYYY-MM-DD]), or relative date.\n"
|
|
@@ -155,7 +169,13 @@ def parse_arguments():
|
|
|
155
169
|
|
|
156
170
|
args = parser.parse_args()
|
|
157
171
|
|
|
158
|
-
|
|
172
|
+
data_args = {
|
|
173
|
+
k: v
|
|
174
|
+
for k, v in vars(args).items()
|
|
175
|
+
if k not in ("json", "raw_json", "init_auth", "version")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if not args.init_auth and not any(data_args.values()):
|
|
159
179
|
parser.error("No arguments provided. At least one argument is required.")
|
|
160
180
|
|
|
161
181
|
return args
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Json Data Formatter
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from rich.console import Console
|
|
6
7
|
from rich.table import Table
|
|
7
8
|
from rich.text import Text
|
|
@@ -9,7 +10,7 @@ from rich.text import Text
|
|
|
9
10
|
CONSOLE = Console()
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def display_user_profile(user_data):
|
|
13
|
+
def display_user_profile(user_data, as_json=False):
|
|
13
14
|
"""User data formatter"""
|
|
14
15
|
|
|
15
16
|
user = user_data["user"]
|
|
@@ -20,6 +21,22 @@ def display_user_profile(user_data):
|
|
|
20
21
|
elif user["weightUnit"] == "US":
|
|
21
22
|
weight_unit = "pounds"
|
|
22
23
|
|
|
24
|
+
if as_json:
|
|
25
|
+
return {
|
|
26
|
+
"user_profile": {
|
|
27
|
+
"first_name": user["firstName"],
|
|
28
|
+
"last_name": user["lastName"],
|
|
29
|
+
"date_of_birth": user["dateOfBirth"],
|
|
30
|
+
"age": user["age"],
|
|
31
|
+
"gender": user["gender"],
|
|
32
|
+
"height": f"{user['height']:.1f} {height_unit}",
|
|
33
|
+
"weight": f"{user['weight']:.1f} {weight_unit}",
|
|
34
|
+
"average_daily_steps": user["averageDailySteps"],
|
|
35
|
+
"member_since": user["memberSince"],
|
|
36
|
+
"timezone": user["timezone"],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
table = Table(title=f"Hello, {user['displayName']} :wave:", show_header=False)
|
|
24
41
|
|
|
25
42
|
table.add_column("")
|
|
@@ -37,11 +54,34 @@ def display_user_profile(user_data):
|
|
|
37
54
|
table.add_row(":clock1: Time Zone", user["timezone"])
|
|
38
55
|
|
|
39
56
|
CONSOLE.print(table)
|
|
57
|
+
return None
|
|
40
58
|
|
|
41
59
|
|
|
42
|
-
def display_sleep(sleep_data):
|
|
60
|
+
def display_sleep(sleep_data, as_json=False):
|
|
43
61
|
"""Sleep data formatter"""
|
|
44
62
|
|
|
63
|
+
if as_json:
|
|
64
|
+
return {
|
|
65
|
+
"sleep": [
|
|
66
|
+
{
|
|
67
|
+
"date": s["dateOfSleep"],
|
|
68
|
+
"deep_minutes": s["levels"]["summary"]
|
|
69
|
+
.get("deep", {})
|
|
70
|
+
.get("minutes"),
|
|
71
|
+
"light_minutes": s["levels"]["summary"]
|
|
72
|
+
.get("light", {})
|
|
73
|
+
.get("minutes"),
|
|
74
|
+
"rem_minutes": s["levels"]["summary"].get("rem", {}).get("minutes"),
|
|
75
|
+
"wake_minutes": s["levels"]["summary"]
|
|
76
|
+
.get("wake", {})
|
|
77
|
+
.get("minutes"),
|
|
78
|
+
"efficiency": s["efficiency"],
|
|
79
|
+
"time_in_bed_hours": round(s["timeInBed"] / 60, 1),
|
|
80
|
+
}
|
|
81
|
+
for s in sleep_data["sleep"]
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
|
|
45
85
|
table = Table(title="Sleep Data Summary :sleeping:", show_header=True)
|
|
46
86
|
|
|
47
87
|
table.add_column("Date :calendar:")
|
|
@@ -64,11 +104,28 @@ def display_sleep(sleep_data):
|
|
|
64
104
|
)
|
|
65
105
|
|
|
66
106
|
CONSOLE.print(table)
|
|
107
|
+
return None
|
|
67
108
|
|
|
68
109
|
|
|
69
|
-
def display_spo2(spo2_data):
|
|
110
|
+
def display_spo2(spo2_data, as_json=False):
|
|
70
111
|
"""SpO2 data formatter"""
|
|
71
112
|
|
|
113
|
+
if isinstance(spo2_data, dict):
|
|
114
|
+
spo2_data = [spo2_data]
|
|
115
|
+
|
|
116
|
+
if as_json:
|
|
117
|
+
return {
|
|
118
|
+
"spo2": [
|
|
119
|
+
{
|
|
120
|
+
"date": s.get("dateTime"),
|
|
121
|
+
"min": s.get("value", {}).get("min"),
|
|
122
|
+
"avg": s.get("value", {}).get("avg"),
|
|
123
|
+
"max": s.get("value", {}).get("max"),
|
|
124
|
+
}
|
|
125
|
+
for s in spo2_data
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
|
|
72
129
|
table = Table(title="SpO2 Data Summary :heart:", show_header=True)
|
|
73
130
|
|
|
74
131
|
table.add_column("Date :calendar:")
|
|
@@ -76,9 +133,6 @@ def display_spo2(spo2_data):
|
|
|
76
133
|
table.add_column("Average :blue_circle:")
|
|
77
134
|
table.add_column("Maximum :green_circle:")
|
|
78
135
|
|
|
79
|
-
if isinstance(spo2_data, dict):
|
|
80
|
-
spo2_data = [spo2_data]
|
|
81
|
-
|
|
82
136
|
for spo2 in spo2_data:
|
|
83
137
|
table.add_row(
|
|
84
138
|
spo2.get("dateTime", "N/A"),
|
|
@@ -88,11 +142,37 @@ def display_spo2(spo2_data):
|
|
|
88
142
|
)
|
|
89
143
|
|
|
90
144
|
CONSOLE.print(table)
|
|
145
|
+
return None
|
|
91
146
|
|
|
92
147
|
|
|
93
|
-
def display_heart_data(heart_data):
|
|
148
|
+
def display_heart_data(heart_data, as_json=False):
|
|
94
149
|
"""Heart data formatter"""
|
|
95
150
|
|
|
151
|
+
if as_json:
|
|
152
|
+
return {
|
|
153
|
+
"heart": [
|
|
154
|
+
{
|
|
155
|
+
"date": a.get("dateTime"),
|
|
156
|
+
"resting_heart_rate": a.get("value", {}).get("restingHeartRate"),
|
|
157
|
+
"zones": [
|
|
158
|
+
{
|
|
159
|
+
"name": z.get("name"),
|
|
160
|
+
"min": z.get("min"),
|
|
161
|
+
"max": z.get("max"),
|
|
162
|
+
"minutes": z.get("minutes"),
|
|
163
|
+
"calories_out": (
|
|
164
|
+
round(z["caloriesOut"], 2)
|
|
165
|
+
if isinstance(z.get("caloriesOut"), (int, float))
|
|
166
|
+
else None
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
for z in a.get("value", {}).get("heartRateZones", [])
|
|
170
|
+
],
|
|
171
|
+
}
|
|
172
|
+
for a in heart_data.get("activities-heart", [])
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
|
|
96
176
|
table = Table(title="Heart Rate Time Series :heart:", show_header=True)
|
|
97
177
|
|
|
98
178
|
table.add_column("Date :calendar:")
|
|
@@ -123,15 +203,30 @@ def display_heart_data(heart_data):
|
|
|
123
203
|
else "N/A"
|
|
124
204
|
),
|
|
125
205
|
)
|
|
126
|
-
|
|
127
206
|
table.add_row(date, str(resting_heart_rate), zones_table)
|
|
128
|
-
|
|
129
207
|
CONSOLE.print(table)
|
|
208
|
+
return None
|
|
130
209
|
|
|
131
210
|
|
|
132
|
-
def display_azm_time_series(azm_data):
|
|
211
|
+
def display_azm_time_series(azm_data, as_json=False):
|
|
133
212
|
"""AZM Time Series data formatter"""
|
|
134
213
|
|
|
214
|
+
if as_json:
|
|
215
|
+
return {
|
|
216
|
+
"active_zone": [
|
|
217
|
+
{
|
|
218
|
+
"date": a.get("dateTime"),
|
|
219
|
+
"active_zone_minutes": a.get("value", {}).get("activeZoneMinutes"),
|
|
220
|
+
"fat_burn_minutes": a.get("value", {}).get(
|
|
221
|
+
"fatBurnActiveZoneMinutes"
|
|
222
|
+
),
|
|
223
|
+
"cardio_minutes": a.get("value", {}).get("cardioActiveZoneMinutes"),
|
|
224
|
+
"peak_minutes": a.get("value", {}).get("peakActiveZoneMinutes"),
|
|
225
|
+
}
|
|
226
|
+
for a in azm_data.get("activities-active-zone-minutes", [])
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
|
|
135
230
|
table = Table(title="AZM Time Series :runner:", show_header=True)
|
|
136
231
|
|
|
137
232
|
table.add_column("Date :calendar:")
|
|
@@ -152,11 +247,23 @@ def display_azm_time_series(azm_data):
|
|
|
152
247
|
)
|
|
153
248
|
|
|
154
249
|
CONSOLE.print(table)
|
|
250
|
+
return None
|
|
155
251
|
|
|
156
252
|
|
|
157
|
-
def display_breathing_rate(breathing_rate_data):
|
|
253
|
+
def display_breathing_rate(breathing_rate_data, as_json=False):
|
|
158
254
|
"""Breathing Rate data formatter"""
|
|
159
255
|
|
|
256
|
+
if as_json:
|
|
257
|
+
return {
|
|
258
|
+
"breathing_rate": [
|
|
259
|
+
{
|
|
260
|
+
"date": br.get("dateTime"),
|
|
261
|
+
"breathing_rate": br.get("value", {}).get("breathingRate"),
|
|
262
|
+
}
|
|
263
|
+
for br in breathing_rate_data.get("br", [])
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
|
|
160
267
|
table = Table(title="Breathing Rate Summary 🫁", show_header=True)
|
|
161
268
|
|
|
162
269
|
table.add_column("Date :calendar:")
|
|
@@ -169,9 +276,10 @@ def display_breathing_rate(breathing_rate_data):
|
|
|
169
276
|
)
|
|
170
277
|
|
|
171
278
|
CONSOLE.print(table)
|
|
279
|
+
return None
|
|
172
280
|
|
|
173
281
|
|
|
174
|
-
def display_devices(devices):
|
|
282
|
+
def display_devices(devices, as_json=False):
|
|
175
283
|
"""Devices list formatter"""
|
|
176
284
|
|
|
177
285
|
def format_mac(mac):
|
|
@@ -179,6 +287,20 @@ def display_devices(devices):
|
|
|
179
287
|
return mac
|
|
180
288
|
return ":".join(mac[i : i + 2] for i in range(0, len(mac), 2))
|
|
181
289
|
|
|
290
|
+
if as_json:
|
|
291
|
+
return {
|
|
292
|
+
"devices": [
|
|
293
|
+
{
|
|
294
|
+
"battery_level": device.get("batteryLevel"),
|
|
295
|
+
"device": device.get("deviceVersion"),
|
|
296
|
+
"type": device.get("type"),
|
|
297
|
+
"last_sync_time": device.get("lastSyncTime"),
|
|
298
|
+
"mac_address": format_mac(str(device.get("mac", "N/A"))),
|
|
299
|
+
}
|
|
300
|
+
for device in devices
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
|
|
182
304
|
table = Table(title="Devices List :link:", show_header=True)
|
|
183
305
|
|
|
184
306
|
table.add_column("Battery % :battery:")
|
|
@@ -190,7 +312,7 @@ def display_devices(devices):
|
|
|
190
312
|
for device in devices:
|
|
191
313
|
mac_address = format_mac(str(device.get("mac", "N/A")))
|
|
192
314
|
table.add_row(
|
|
193
|
-
f"{str(device.get(
|
|
315
|
+
f"{str(device.get('batteryLevel', 'N/A'))}%",
|
|
194
316
|
str(device.get("deviceVersion", "N/A")),
|
|
195
317
|
str(device.get("type", "N/A")),
|
|
196
318
|
str(device.get("lastSyncTime", "N/A")),
|
|
@@ -198,14 +320,39 @@ def display_devices(devices):
|
|
|
198
320
|
)
|
|
199
321
|
|
|
200
322
|
CONSOLE.print(table)
|
|
323
|
+
return None
|
|
201
324
|
|
|
202
325
|
|
|
203
|
-
def display_activity(activity_data, unit_system):
|
|
326
|
+
def display_activity(activity_data, unit_system, as_json=False):
|
|
204
327
|
"""Activity data formatter"""
|
|
205
328
|
|
|
206
|
-
dis_unit = "km"
|
|
207
|
-
|
|
208
|
-
|
|
329
|
+
dis_unit = "km" if unit_system != "US" else "miles"
|
|
330
|
+
|
|
331
|
+
if as_json:
|
|
332
|
+
return {
|
|
333
|
+
"activities": [
|
|
334
|
+
{
|
|
335
|
+
"date": day.get("date"),
|
|
336
|
+
"activities": [
|
|
337
|
+
{
|
|
338
|
+
"start_time": a.get("startTime"),
|
|
339
|
+
"name": a.get("name"),
|
|
340
|
+
"description": a.get("description"),
|
|
341
|
+
"distance": (
|
|
342
|
+
f"{a.get('distance', 'N/A')} {dis_unit}"
|
|
343
|
+
if a.get("distance") is not None
|
|
344
|
+
else None
|
|
345
|
+
),
|
|
346
|
+
"steps": a.get("steps"),
|
|
347
|
+
"calories": a.get("calories"),
|
|
348
|
+
"duration_minutes": round(a.get("duration", 0) / 60000, 1),
|
|
349
|
+
}
|
|
350
|
+
for a in day.get("activities", [])
|
|
351
|
+
],
|
|
352
|
+
}
|
|
353
|
+
for day in activity_data
|
|
354
|
+
]
|
|
355
|
+
}
|
|
209
356
|
|
|
210
357
|
table = Table(title="Daily Activities :runner:", show_header=True)
|
|
211
358
|
|
|
@@ -236,3 +383,4 @@ def display_activity(activity_data, unit_system):
|
|
|
236
383
|
table.add_row(activity_day.get("date", ""), activity_table)
|
|
237
384
|
|
|
238
385
|
CONSOLE.print(table)
|
|
386
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Main Module
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from . import fitbit_setup as setup
|
|
7
|
+
from . import output
|
|
8
|
+
from .cli import parse_arguments
|
|
9
|
+
from .fitbit_api import FitbitAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
"""Main function"""
|
|
14
|
+
|
|
15
|
+
args = parse_arguments()
|
|
16
|
+
|
|
17
|
+
if args.init_auth:
|
|
18
|
+
setup.fitbit_init_setup()
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
credentials = setup.read_fitbit_token()
|
|
22
|
+
|
|
23
|
+
fitbit = FitbitAPI(
|
|
24
|
+
client_id=credentials["client_id"],
|
|
25
|
+
client_secret=credentials["secret"],
|
|
26
|
+
access_token=credentials["access_token"],
|
|
27
|
+
refresh_token=credentials["refresh_token"],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if args.raw_json:
|
|
31
|
+
output.raw_json_display(fitbit, args)
|
|
32
|
+
elif args.json:
|
|
33
|
+
output.json_display(fitbit, args)
|
|
34
|
+
else:
|
|
35
|
+
output.table_display(fitbit, args)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Output modes for the Fitbit CLI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from datetime import date, datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from . import formatter as fmt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def collect_activities(fitbit, args):
|
|
13
|
+
"""Fetch activity data for a date or date range."""
|
|
14
|
+
start_date, end_date = args.activities
|
|
15
|
+
if end_date is None:
|
|
16
|
+
data = fitbit.get_daily_activity_summary(str(start_date))
|
|
17
|
+
return [{**data, "date": str(start_date)}]
|
|
18
|
+
start = (
|
|
19
|
+
start_date
|
|
20
|
+
if isinstance(start_date, date)
|
|
21
|
+
else datetime.strptime(start_date, "%Y-%m-%d").date()
|
|
22
|
+
)
|
|
23
|
+
end = (
|
|
24
|
+
end_date
|
|
25
|
+
if isinstance(end_date, date)
|
|
26
|
+
else datetime.strptime(end_date, "%Y-%m-%d").date()
|
|
27
|
+
)
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
**fitbit.get_daily_activity_summary(
|
|
31
|
+
(start + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
32
|
+
),
|
|
33
|
+
"date": (start + timedelta(days=i)).strftime("%Y-%m-%d"),
|
|
34
|
+
}
|
|
35
|
+
for i in range((end - start).days + 1)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def json_display(fitbit, args):
|
|
40
|
+
"""Fetch data and render each requested endpoint as a single JSON object to stdout."""
|
|
41
|
+
result = {}
|
|
42
|
+
profile = None
|
|
43
|
+
|
|
44
|
+
if args.user_profile:
|
|
45
|
+
profile = fitbit.get_user_profile()
|
|
46
|
+
result.update(fmt.display_user_profile(profile, as_json=True))
|
|
47
|
+
if args.devices:
|
|
48
|
+
result.update(fmt.display_devices(fitbit.get_devices(), as_json=True))
|
|
49
|
+
if args.sleep:
|
|
50
|
+
result.update(
|
|
51
|
+
fmt.display_sleep(fitbit.get_sleep_log(*args.sleep), as_json=True)
|
|
52
|
+
)
|
|
53
|
+
if args.spo2:
|
|
54
|
+
result.update(
|
|
55
|
+
fmt.display_spo2(fitbit.get_spo2_summary(*args.spo2), as_json=True)
|
|
56
|
+
)
|
|
57
|
+
if args.heart:
|
|
58
|
+
result.update(
|
|
59
|
+
fmt.display_heart_data(
|
|
60
|
+
fitbit.get_heart_rate_time_series(*args.heart), as_json=True
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
if args.active_zone:
|
|
64
|
+
result.update(
|
|
65
|
+
fmt.display_azm_time_series(
|
|
66
|
+
fitbit.get_azm_time_series(*args.active_zone), as_json=True
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
if args.breathing_rate:
|
|
70
|
+
result.update(
|
|
71
|
+
fmt.display_breathing_rate(
|
|
72
|
+
fitbit.get_breathing_rate_summary(*args.breathing_rate), as_json=True
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
if args.activities:
|
|
76
|
+
activity_data = collect_activities(fitbit, args)
|
|
77
|
+
if profile is None:
|
|
78
|
+
profile = fitbit.get_user_profile()
|
|
79
|
+
unit_system = profile.get("user", {}).get("distanceUnit", "METRIC")
|
|
80
|
+
result.update(fmt.display_activity(activity_data, unit_system, as_json=True))
|
|
81
|
+
|
|
82
|
+
print(json.dumps(result, separators=(",", ":")))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def raw_json_display(fitbit, args):
|
|
86
|
+
"""Collect raw API responses and print compact JSON to stdout."""
|
|
87
|
+
result = {}
|
|
88
|
+
|
|
89
|
+
if args.user_profile:
|
|
90
|
+
result["user_profile"] = fitbit.get_user_profile()
|
|
91
|
+
if args.devices:
|
|
92
|
+
result["devices"] = fitbit.get_devices()
|
|
93
|
+
if args.sleep:
|
|
94
|
+
result["sleep"] = fitbit.get_sleep_log(*args.sleep)
|
|
95
|
+
if args.spo2:
|
|
96
|
+
result["spo2"] = fitbit.get_spo2_summary(*args.spo2)
|
|
97
|
+
if args.heart:
|
|
98
|
+
result["heart"] = fitbit.get_heart_rate_time_series(*args.heart)
|
|
99
|
+
if args.active_zone:
|
|
100
|
+
result["active_zone"] = fitbit.get_azm_time_series(*args.active_zone)
|
|
101
|
+
if args.breathing_rate:
|
|
102
|
+
result["breathing_rate"] = fitbit.get_breathing_rate_summary(
|
|
103
|
+
*args.breathing_rate
|
|
104
|
+
)
|
|
105
|
+
if args.activities:
|
|
106
|
+
result["activities"] = collect_activities(fitbit, args)
|
|
107
|
+
|
|
108
|
+
print(json.dumps(result, separators=(",", ":")))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def table_display(fitbit, args):
|
|
112
|
+
"""Fetch data and render rich tables to the terminal."""
|
|
113
|
+
with fmt.CONSOLE.status("[bold green]Fetching data...") as _:
|
|
114
|
+
profile = None
|
|
115
|
+
if args.user_profile:
|
|
116
|
+
profile = fitbit.get_user_profile()
|
|
117
|
+
fmt.display_user_profile(profile)
|
|
118
|
+
if args.devices:
|
|
119
|
+
fmt.display_devices(fitbit.get_devices())
|
|
120
|
+
if args.sleep:
|
|
121
|
+
fmt.display_sleep(fitbit.get_sleep_log(*args.sleep))
|
|
122
|
+
if args.spo2:
|
|
123
|
+
fmt.display_spo2(fitbit.get_spo2_summary(*args.spo2))
|
|
124
|
+
if args.heart:
|
|
125
|
+
fmt.display_heart_data(fitbit.get_heart_rate_time_series(*args.heart))
|
|
126
|
+
if args.active_zone:
|
|
127
|
+
fmt.display_azm_time_series(fitbit.get_azm_time_series(*args.active_zone))
|
|
128
|
+
if args.breathing_rate:
|
|
129
|
+
fmt.display_breathing_rate(
|
|
130
|
+
fitbit.get_breathing_rate_summary(*args.breathing_rate)
|
|
131
|
+
)
|
|
132
|
+
if args.activities:
|
|
133
|
+
activity_data = collect_activities(fitbit, args)
|
|
134
|
+
if profile is None:
|
|
135
|
+
profile = fitbit.get_user_profile()
|
|
136
|
+
unit_system = profile.get("user", {}).get("distanceUnit", "METRIC")
|
|
137
|
+
fmt.display_activity(activity_data, unit_system)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fitbit-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Access your Fitbit data at your terminal.
|
|
5
5
|
Home-page: https://github.com/veerendra2/fitbit-cli
|
|
6
|
-
Download-URL: https://github.com/veerendra2/fitbit-cli/archive/1.
|
|
6
|
+
Download-URL: https://github.com/veerendra2/fitbit-cli/archive/1.6.0.tar.gz
|
|
7
7
|
Author: veerendra2
|
|
8
8
|
Author-email: vk.tyk23@simplelogin.com
|
|
9
9
|
License: MIT
|
|
@@ -11,22 +11,21 @@ Project-URL: Documentation, https://github.com/veerendra2/fitbit-cli
|
|
|
11
11
|
Keywords: fitbit,fitbit-api,cli,python
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
-
Classifier: License :: OSI Approved ::
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Natural Language :: English
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.
|
|
22
|
-
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
22
|
Classifier: Programming Language :: Python
|
|
24
23
|
Classifier: Topic :: Utilities
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.12
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist: requests==2.
|
|
29
|
-
Requires-Dist: rich==14.3.
|
|
27
|
+
Requires-Dist: requests==2.33.0
|
|
28
|
+
Requires-Dist: rich==14.3.3
|
|
30
29
|
Dynamic: author
|
|
31
30
|
Dynamic: author-email
|
|
32
31
|
Dynamic: classifier
|
|
@@ -49,10 +48,12 @@ Dynamic: summary
|
|
|
49
48
|
](https://pypi.org/project/fitbit-cli/) [
|
|
50
49
|
](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
|
|
51
50
|
|
|
52
|
-
>
|
|
51
|
+
> _This is not an official Fitbit CLI_
|
|
53
52
|
|
|
54
53
|
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!
|
|
55
54
|
|
|
55
|
+
> **AI agent-friendly** 🤖 — since v1.6.0, use `--json` for minimized, token-efficient JSON output or `--raw-json` for the full Fitbit API response. No spinners, pure JSON.
|
|
56
|
+
|
|
56
57
|
<p align="center">
|
|
57
58
|
<img alt="Fitbit logo", width="350" src="https://raw.githubusercontent.com/veerendra2/fitbit-cli/refs/heads/main/assets/Fitbit_Logo_White_RGB.jpg">
|
|
58
59
|
</p>
|
|
@@ -86,13 +87,16 @@ python -m pip install fitbit-cli
|
|
|
86
87
|
|
|
87
88
|
```bash
|
|
88
89
|
fitbit-cli -h
|
|
89
|
-
usage: fitbit-cli [-h] [-i] [-
|
|
90
|
+
usage: fitbit-cli [-h] [-i] [-j] [-r] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]]
|
|
91
|
+
[-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
|
|
90
92
|
|
|
91
93
|
Fitbit CLI -- Access your Fitbit data at your terminal.
|
|
92
94
|
|
|
93
95
|
options:
|
|
94
96
|
-h, --help show this help message and exit
|
|
95
97
|
-i, --init-auth Initialize Fitbit iterative authentication setup
|
|
98
|
+
-j, --json Output table data as JSON.
|
|
99
|
+
-r, --raw-json Output raw JSON from the Fitbit API.
|
|
96
100
|
-v, --version Show fitbit-cli version
|
|
97
101
|
|
|
98
102
|
APIs:
|
|
@@ -34,22 +34,21 @@ setup(
|
|
|
34
34
|
classifiers=[
|
|
35
35
|
"Development Status :: 5 - Production/Stable",
|
|
36
36
|
"Intended Audience :: End Users/Desktop",
|
|
37
|
-
"License :: OSI Approved ::
|
|
37
|
+
"License :: OSI Approved :: MIT License",
|
|
38
38
|
"Natural Language :: English",
|
|
39
39
|
"Operating System :: MacOS",
|
|
40
40
|
"Operating System :: POSIX :: Linux",
|
|
41
41
|
"Programming Language :: Python :: 3 :: Only",
|
|
42
42
|
"Programming Language :: Python :: 3.12",
|
|
43
43
|
"Programming Language :: Python :: 3.13",
|
|
44
|
-
"Programming Language :: Python :: 3.
|
|
45
|
-
"Programming Language :: Python :: 3",
|
|
44
|
+
"Programming Language :: Python :: 3.14",
|
|
46
45
|
"Programming Language :: Python",
|
|
47
46
|
"Topic :: Utilities",
|
|
48
47
|
],
|
|
49
48
|
install_requires=[
|
|
50
|
-
"requests==2.
|
|
51
|
-
"rich==14.3.
|
|
49
|
+
"requests==2.33.0",
|
|
50
|
+
"rich==14.3.3",
|
|
52
51
|
],
|
|
53
|
-
python_requires=">=3.
|
|
52
|
+
python_requires=">=3.12",
|
|
54
53
|
entry_points={"console_scripts": ["fitbit-cli = fitbit_cli.main:main"]},
|
|
55
54
|
)
|
|
@@ -13,7 +13,12 @@ from unittest.mock import patch
|
|
|
13
13
|
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
|
14
14
|
|
|
15
15
|
# pylint: disable=C0413
|
|
16
|
-
from fitbit_cli.cli import
|
|
16
|
+
from fitbit_cli.cli import (
|
|
17
|
+
_get_date_range,
|
|
18
|
+
_parse_relative_dates,
|
|
19
|
+
parse_arguments,
|
|
20
|
+
parse_date_range,
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class TestCLIDateFunctions(unittest.TestCase):
|
|
@@ -148,6 +153,38 @@ class TestCLIDateFunctions(unittest.TestCase):
|
|
|
148
153
|
with self.assertRaises(ValueError):
|
|
149
154
|
parse_date_range("invalid-date-format")
|
|
150
155
|
|
|
156
|
+
@patch("sys.argv", ["fitbit-cli", "--json"])
|
|
157
|
+
def test_json_flag_alone_raises_error(self):
|
|
158
|
+
"""Test that --json alone (without a data flag) triggers the no-arguments error."""
|
|
159
|
+
with self.assertRaises(SystemExit):
|
|
160
|
+
parse_arguments()
|
|
161
|
+
|
|
162
|
+
@patch("sys.argv", ["fitbit-cli", "--raw-json"])
|
|
163
|
+
def test_raw_json_flag_alone_raises_error(self):
|
|
164
|
+
"""Test that --raw-json alone (without a data flag) triggers the no-arguments error."""
|
|
165
|
+
with self.assertRaises(SystemExit):
|
|
166
|
+
parse_arguments()
|
|
167
|
+
|
|
168
|
+
@patch("sys.argv", ["fitbit-cli", "--json", "--user-profile"])
|
|
169
|
+
def test_json_with_data_flag_parses_successfully(self):
|
|
170
|
+
"""Test that --json combined with a data flag parses without error."""
|
|
171
|
+
args = parse_arguments()
|
|
172
|
+
self.assertTrue(args.json)
|
|
173
|
+
self.assertTrue(args.user_profile)
|
|
174
|
+
|
|
175
|
+
@patch("sys.argv", ["fitbit-cli", "--raw-json", "--devices"])
|
|
176
|
+
def test_raw_json_with_data_flag_parses_successfully(self):
|
|
177
|
+
"""Test that --raw-json combined with a data flag parses without error."""
|
|
178
|
+
args = parse_arguments()
|
|
179
|
+
self.assertTrue(args.raw_json)
|
|
180
|
+
self.assertTrue(args.devices)
|
|
181
|
+
|
|
182
|
+
@patch("sys.argv", ["fitbit-cli", "--init-auth"])
|
|
183
|
+
def test_init_auth_alone_parses_successfully(self):
|
|
184
|
+
"""Test that --init-auth alone does not trigger the no-arguments error."""
|
|
185
|
+
args = parse_arguments()
|
|
186
|
+
self.assertTrue(args.init_auth)
|
|
187
|
+
|
|
151
188
|
|
|
152
189
|
if __name__ == "__main__":
|
|
153
190
|
unittest.main()
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
Main Module
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from datetime import datetime, timedelta
|
|
7
|
-
|
|
8
|
-
from . import fitbit_setup as setup
|
|
9
|
-
from . import formatter as fmt
|
|
10
|
-
from .cli import parse_arguments
|
|
11
|
-
from .fitbit_api import FitbitAPI
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def main():
|
|
15
|
-
"""Main function"""
|
|
16
|
-
|
|
17
|
-
args = parse_arguments()
|
|
18
|
-
|
|
19
|
-
if args.init_auth:
|
|
20
|
-
setup.fitbit_init_setup()
|
|
21
|
-
|
|
22
|
-
credentials = setup.read_fitbit_token()
|
|
23
|
-
|
|
24
|
-
fitbit = FitbitAPI(
|
|
25
|
-
client_id=credentials["client_id"],
|
|
26
|
-
client_secret=credentials["secret"],
|
|
27
|
-
access_token=credentials["access_token"],
|
|
28
|
-
refresh_token=credentials["refresh_token"],
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
with fmt.CONSOLE.status("[bold green]Fetching data...") as _:
|
|
32
|
-
if args.user_profile:
|
|
33
|
-
fmt.display_user_profile(fitbit.get_user_profile())
|
|
34
|
-
if args.devices:
|
|
35
|
-
fmt.display_devices(fitbit.get_devices())
|
|
36
|
-
if args.sleep:
|
|
37
|
-
fmt.display_sleep(fitbit.get_sleep_log(*args.sleep))
|
|
38
|
-
if args.spo2:
|
|
39
|
-
fmt.display_spo2(fitbit.get_spo2_summary(*args.spo2))
|
|
40
|
-
if args.heart:
|
|
41
|
-
fmt.display_heart_data(fitbit.get_heart_rate_time_series(*args.heart))
|
|
42
|
-
if args.active_zone:
|
|
43
|
-
fmt.display_azm_time_series(fitbit.get_azm_time_series(*args.active_zone))
|
|
44
|
-
if args.breathing_rate:
|
|
45
|
-
fmt.display_breathing_rate(
|
|
46
|
-
fitbit.get_breathing_rate_summary(*args.breathing_rate)
|
|
47
|
-
)
|
|
48
|
-
if args.activities:
|
|
49
|
-
start_date, end_date = args.activities
|
|
50
|
-
activity_data = []
|
|
51
|
-
|
|
52
|
-
# Get unit system from profile
|
|
53
|
-
unit_system = (
|
|
54
|
-
fitbit.get_user_profile().get("user", "").get("distanceUnit", "METRIC")
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
# 'Get Daily Activity Summary' API only accepts single date
|
|
58
|
-
# So we either fetch one date or iterate through a range
|
|
59
|
-
if end_date is None:
|
|
60
|
-
data = fitbit.get_daily_activity_summary(start_date)
|
|
61
|
-
data["date"] = str(start_date)
|
|
62
|
-
activity_data = [data]
|
|
63
|
-
else:
|
|
64
|
-
start = datetime.strptime(start_date, "%Y-%m-%d")
|
|
65
|
-
end = datetime.strptime(end_date, "%Y-%m-%d")
|
|
66
|
-
activity_data = [
|
|
67
|
-
{
|
|
68
|
-
**fitbit.get_daily_activity_summary(
|
|
69
|
-
(start + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
70
|
-
),
|
|
71
|
-
"date": (start + timedelta(days=i)).strftime("%Y-%m-%d"),
|
|
72
|
-
}
|
|
73
|
-
for i in range((end - start).days + 1)
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
fmt.display_activity(activity_data, unit_system)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|