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.
Files changed (24) hide show
  1. {fitbit_cli-1.5.2/fitbit_cli.egg-info → fitbit_cli-1.6.0}/PKG-INFO +14 -10
  2. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/README.md +7 -2
  3. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/__init__.py +1 -1
  4. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/cli.py +21 -1
  5. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/formatter.py +165 -17
  6. fitbit_cli-1.6.0/fitbit_cli/main.py +35 -0
  7. fitbit_cli-1.6.0/fitbit_cli/output.py +137 -0
  8. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0/fitbit_cli.egg-info}/PKG-INFO +14 -10
  9. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/SOURCES.txt +1 -0
  10. fitbit_cli-1.6.0/fitbit_cli.egg-info/requires.txt +2 -0
  11. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/setup.py +5 -6
  12. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/tests/cli_test.py +38 -1
  13. fitbit_cli-1.5.2/fitbit_cli/main.py +0 -76
  14. fitbit_cli-1.5.2/fitbit_cli.egg-info/requires.txt +0 -2
  15. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/LICENSE +0 -0
  16. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/MANIFEST.in +0 -0
  17. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/exceptions.py +0 -0
  18. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/fitbit_api.py +0 -0
  19. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli/fitbit_setup.py +0 -0
  20. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/dependency_links.txt +0 -0
  21. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/entry_points.txt +0 -0
  22. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/fitbit_cli.egg-info/top_level.txt +0 -0
  23. {fitbit_cli-1.5.2 → fitbit_cli-1.6.0}/pyproject.toml +0 -0
  24. {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.5.2
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.5.2.tar.gz
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 :: Apache Software License
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.9
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.9
24
+ Requires-Python: >=3.12
26
25
  Description-Content-Type: text/markdown
27
26
  License-File: LICENSE
28
- Requires-Dist: requests==2.32.5
29
- Requires-Dist: rich==14.3.2
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/) [![ClickPy Stats](https://img.shields.io/badge/ClickPy%20Stats-A5951E)
50
49
  ](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
51
50
 
52
- > This is not an official Fitbit CLI
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] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]] [-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
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/) [![ClickPy Stats](https://img.shields.io/badge/ClickPy%20Stats-A5951E)
6
6
  ](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
7
7
 
8
- > This is not an official Fitbit CLI
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] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]] [-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
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:
@@ -3,4 +3,4 @@
3
3
  fitbit_cli Module
4
4
  """
5
5
 
6
- __version__ = "1.5.2"
6
+ __version__ = "1.6.0"
@@ -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
- if not any(vars(args).values()):
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("batteryLevel", "N/A"))}%",
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
- if unit_system == "US":
208
- dis_unit = "miles"
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.5.2
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.5.2.tar.gz
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 :: Apache Software License
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.9
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.9
24
+ Requires-Python: >=3.12
26
25
  Description-Content-Type: text/markdown
27
26
  License-File: LICENSE
28
- Requires-Dist: requests==2.32.5
29
- Requires-Dist: rich==14.3.2
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/) [![ClickPy Stats](https://img.shields.io/badge/ClickPy%20Stats-A5951E)
50
49
  ](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
51
50
 
52
- > This is not an official Fitbit CLI
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] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]] [-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
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:
@@ -10,6 +10,7 @@ fitbit_cli/fitbit_api.py
10
10
  fitbit_cli/fitbit_setup.py
11
11
  fitbit_cli/formatter.py
12
12
  fitbit_cli/main.py
13
+ fitbit_cli/output.py
13
14
  fitbit_cli.egg-info/PKG-INFO
14
15
  fitbit_cli.egg-info/SOURCES.txt
15
16
  fitbit_cli.egg-info/dependency_links.txt
@@ -0,0 +1,2 @@
1
+ requests==2.33.0
2
+ rich==14.3.3
@@ -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 :: Apache Software License",
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.9",
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.32.5",
51
- "rich==14.3.2",
49
+ "requests==2.33.0",
50
+ "rich==14.3.3",
52
51
  ],
53
- python_requires=">=3.9",
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 _get_date_range, _parse_relative_dates, parse_date_range
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)
@@ -1,2 +0,0 @@
1
- requests==2.32.5
2
- rich==14.3.2
File without changes
File without changes
File without changes
File without changes