garmin-py 2.7.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 (82) hide show
  1. garmin_py-2.7.0/LICENSE +21 -0
  2. garmin_py-2.7.0/PKG-INFO +470 -0
  3. garmin_py-2.7.0/README.md +445 -0
  4. garmin_py-2.7.0/pyproject.toml +76 -0
  5. garmin_py-2.7.0/setup.cfg +4 -0
  6. garmin_py-2.7.0/src/garmin_cli/__init__.py +7 -0
  7. garmin_py-2.7.0/src/garmin_cli/__main__.py +4 -0
  8. garmin_py-2.7.0/src/garmin_cli/_env.py +22 -0
  9. garmin_py-2.7.0/src/garmin_cli/auth.py +198 -0
  10. garmin_py-2.7.0/src/garmin_cli/backend.py +355 -0
  11. garmin_py-2.7.0/src/garmin_cli/cli.py +185 -0
  12. garmin_py-2.7.0/src/garmin_cli/commands/__init__.py +1 -0
  13. garmin_py-2.7.0/src/garmin_cli/commands/_options.py +89 -0
  14. garmin_py-2.7.0/src/garmin_cli/commands/activities.py +406 -0
  15. garmin_py-2.7.0/src/garmin_cli/commands/devices.py +27 -0
  16. garmin_py-2.7.0/src/garmin_cli/commands/health.py +347 -0
  17. garmin_py-2.7.0/src/garmin_cli/commands/login.py +124 -0
  18. garmin_py-2.7.0/src/garmin_cli/commands/performance.py +156 -0
  19. garmin_py-2.7.0/src/garmin_cli/commands/workouts.py +220 -0
  20. garmin_py-2.7.0/src/garmin_cli/config.py +34 -0
  21. garmin_py-2.7.0/src/garmin_cli/date_utils.py +115 -0
  22. garmin_py-2.7.0/src/garmin_cli/endpoints/__init__.py +1 -0
  23. garmin_py-2.7.0/src/garmin_cli/endpoints/_base.py +258 -0
  24. garmin_py-2.7.0/src/garmin_cli/endpoints/activities.py +344 -0
  25. garmin_py-2.7.0/src/garmin_cli/endpoints/devices.py +19 -0
  26. garmin_py-2.7.0/src/garmin_cli/endpoints/health.py +127 -0
  27. garmin_py-2.7.0/src/garmin_cli/endpoints/metrics.py +48 -0
  28. garmin_py-2.7.0/src/garmin_cli/endpoints/performance.py +138 -0
  29. garmin_py-2.7.0/src/garmin_cli/endpoints/workouts.py +150 -0
  30. garmin_py-2.7.0/src/garmin_cli/exceptions.py +30 -0
  31. garmin_py-2.7.0/src/garmin_cli/input_reader.py +67 -0
  32. garmin_py-2.7.0/src/garmin_cli/mcp_auth.py +43 -0
  33. garmin_py-2.7.0/src/garmin_cli/mcp_cli.py +258 -0
  34. garmin_py-2.7.0/src/garmin_cli/mcp_server.py +56 -0
  35. garmin_py-2.7.0/src/garmin_cli/mcp_tools/__init__.py +10 -0
  36. garmin_py-2.7.0/src/garmin_cli/mcp_tools/_shared.py +257 -0
  37. garmin_py-2.7.0/src/garmin_cli/mcp_tools/activities.py +161 -0
  38. garmin_py-2.7.0/src/garmin_cli/mcp_tools/activities_write.py +260 -0
  39. garmin_py-2.7.0/src/garmin_cli/mcp_tools/health.py +113 -0
  40. garmin_py-2.7.0/src/garmin_cli/mcp_tools/misc.py +164 -0
  41. garmin_py-2.7.0/src/garmin_cli/mcp_tools/performance.py +91 -0
  42. garmin_py-2.7.0/src/garmin_cli/mcp_tools/workouts.py +305 -0
  43. garmin_py-2.7.0/src/garmin_cli/metrics/__init__.py +42 -0
  44. garmin_py-2.7.0/src/garmin_cli/metrics/field_table.py +152 -0
  45. garmin_py-2.7.0/src/garmin_cli/metrics/registry.py +420 -0
  46. garmin_py-2.7.0/src/garmin_cli/metrics/sport_profile.py +247 -0
  47. garmin_py-2.7.0/src/garmin_cli/output.py +158 -0
  48. garmin_py-2.7.0/src/garmin_cli/serializers/__init__.py +238 -0
  49. garmin_py-2.7.0/src/garmin_cli/serializers/_common.py +133 -0
  50. garmin_py-2.7.0/src/garmin_cli/serializers/activities.py +611 -0
  51. garmin_py-2.7.0/src/garmin_cli/serializers/devices.py +32 -0
  52. garmin_py-2.7.0/src/garmin_cli/serializers/health.py +422 -0
  53. garmin_py-2.7.0/src/garmin_cli/serializers/performance.py +306 -0
  54. garmin_py-2.7.0/src/garmin_cli/serializers/workouts.py +155 -0
  55. garmin_py-2.7.0/src/garmin_cli/services/__init__.py +8 -0
  56. garmin_py-2.7.0/src/garmin_cli/services/activities.py +139 -0
  57. garmin_py-2.7.0/src/garmin_cli/services/performance.py +37 -0
  58. garmin_py-2.7.0/src/garmin_cli/token_store.py +93 -0
  59. garmin_py-2.7.0/src/garmin_cli/units.py +90 -0
  60. garmin_py-2.7.0/src/garmin_cli/workout_builder.py +267 -0
  61. garmin_py-2.7.0/src/garmin_cli/workout_schema.py +185 -0
  62. garmin_py-2.7.0/src/garmin_py.egg-info/PKG-INFO +470 -0
  63. garmin_py-2.7.0/src/garmin_py.egg-info/SOURCES.txt +80 -0
  64. garmin_py-2.7.0/src/garmin_py.egg-info/dependency_links.txt +1 -0
  65. garmin_py-2.7.0/src/garmin_py.egg-info/entry_points.txt +2 -0
  66. garmin_py-2.7.0/src/garmin_py.egg-info/requires.txt +16 -0
  67. garmin_py-2.7.0/src/garmin_py.egg-info/top_level.txt +1 -0
  68. garmin_py-2.7.0/tests/test_activity_data_gaps.py +193 -0
  69. garmin_py-2.7.0/tests/test_auth.py +639 -0
  70. garmin_py-2.7.0/tests/test_backend.py +443 -0
  71. garmin_py-2.7.0/tests/test_cli_lazy.py +66 -0
  72. garmin_py-2.7.0/tests/test_config.py +108 -0
  73. garmin_py-2.7.0/tests/test_date_utils.py +244 -0
  74. garmin_py-2.7.0/tests/test_e2e_conftest.py +85 -0
  75. garmin_py-2.7.0/tests/test_input_reader.py +115 -0
  76. garmin_py-2.7.0/tests/test_mcp_auth.py +82 -0
  77. garmin_py-2.7.0/tests/test_output.py +350 -0
  78. garmin_py-2.7.0/tests/test_packaging.py +66 -0
  79. garmin_py-2.7.0/tests/test_serializers.py +1632 -0
  80. garmin_py-2.7.0/tests/test_token_store.py +42 -0
  81. garmin_py-2.7.0/tests/test_workout_builder.py +435 -0
  82. garmin_py-2.7.0/tests/test_workout_schema.py +369 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ching Kuo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,470 @@
1
+ Metadata-Version: 2.4
2
+ Name: garmin-py
3
+ Version: 2.7.0
4
+ Summary: CLI tool for extracting health and activity data from Garmin Connect
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/ching-kuo/garmin-py
7
+ Project-URL: Repository, https://github.com/ching-kuo/garmin-py
8
+ Project-URL: Changelog, https://github.com/ching-kuo/garmin-py/blob/main/CHANGELOG.md
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: garminconnect==0.3.2
13
+ Requires-Dist: click>=8.0
14
+ Requires-Dist: tabulate>=0.9
15
+ Requires-Dist: pyyaml>=6.0
16
+ Provides-Extra: mcp
17
+ Requires-Dist: mcp[cli]<3,>=2.0.0b1; extra == "mcp"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=9.0; extra == "dev"
20
+ Requires-Dist: pytest-mock>=3.15; extra == "dev"
21
+ Requires-Dist: ruff>=0.4; extra == "dev"
22
+ Requires-Dist: mypy>=1.8; extra == "dev"
23
+ Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # garmin-cli
27
+
28
+ A command-line tool for extracting health, activity, workout, and performance data from Garmin Connect. Designed for both human use (table output) and LLM agent consumption (JSON output).
29
+
30
+ ## Installation
31
+
32
+ From PyPI (the distribution is `garmin-py`; the installed command is `garmin-cli`):
33
+
34
+ ```bash
35
+ pip install garmin-py
36
+ ```
37
+
38
+ Or from a local checkout:
39
+
40
+ ```bash
41
+ pip install .
42
+ ```
43
+
44
+ Verify the installation:
45
+
46
+ ```bash
47
+ garmin-cli --version
48
+ ```
49
+
50
+ ## Authentication
51
+
52
+ `garmin-cli` authenticates via the maintained [`python-garminconnect`](https://github.com/cyberjunky/python-garminconnect) backend. The primary session-home surface is now `GARMIN_HOME` / `--garmin-home`; `GARTH_HOME` / `--garth-home` remains as a deprecated compatibility alias.
53
+
54
+ ### Interactive login (recommended)
55
+
56
+ ```bash
57
+ garmin-cli login
58
+ # Email: your@email.com
59
+ # Password: (hidden)
60
+ # Login successful. Token store saved at: ~/.garminconnect/garmin_tokens.json
61
+ ```
62
+
63
+ Use `--email` / `--password` to skip the prompts (useful for scripting):
64
+
65
+ ```bash
66
+ garmin-cli login --email your@email.com --password yourpassword
67
+ ```
68
+
69
+ Check login status at any time:
70
+
71
+ ```bash
72
+ garmin-cli login status
73
+ # Logged in. Token store at: ~/.garminconnect/garmin_tokens.json
74
+
75
+ garmin-cli --json login status
76
+ # {"ok": true, "command": "login status", "count": 1, "data": [{"authenticated": true, "garmin_home": "..."}]}
77
+ ```
78
+
79
+ ### Environment variables (alternative)
80
+
81
+ ```bash
82
+ export GARMIN_EMAIL="your@email.com"
83
+ export GARMIN_PASSWORD="yourpassword"
84
+ garmin-cli health sleep --days 1
85
+ ```
86
+
87
+ | Variable | Default | Description |
88
+ |----------|---------|-------------|
89
+ | `GARMIN_EMAIL` | — | Account email for credential-based login |
90
+ | `GARMIN_PASSWORD` | — | Account password for credential-based login |
91
+ | `GARMIN_CLI_HTTP_TIMEOUT` | `30` | HTTP request timeout in seconds (float); invalid or non-positive values fall back to `30` |
92
+ | `GARMIN_CLI_RETRY_DELAYS` | `2,4,8` | Comma-separated retry delay sequence in seconds (e.g. `1,2,4`); invalid values fall back to `2,4,8` |
93
+ | `GARMIN_CLI_AUTH_PROBE_TTL` | `600` | Seconds to cache a successful auth probe in the MCP server (float); `0` disables caching and probes on every call |
94
+ | `GARMIN_CLI_DAILY_CALL_DELAY` | `0.5` | Delay in seconds (float) between request submissions for endpoints that fan out one request per day (e.g. `daily-summary`); invalid or negative values fall back to `0.5` |
95
+ | `GARMIN_CLI_FETCH_CONCURRENCY` | `4` | Worker cap for concurrent fan-out fetches (per-day ranges, report snapshot sections, multisport children); invalid or non-positive values fall back to `4` |
96
+
97
+ ### Custom session directory
98
+
99
+ ```bash
100
+ garmin-cli --garmin-home /path/to/session login
101
+ garmin-cli --garmin-home /path/to/session health sleep --days 1
102
+ ```
103
+
104
+ The default session directory is `~/.garminconnect`. It is created with `0o700` permissions. Symlinks are rejected. Do not share this directory.
105
+
106
+ New sessions are stored as `garmin_tokens.json` inside `GARMIN_HOME`. If you still have an existing `~/.garth/garmin_tokens.json`, the CLI will copy it into the new default home on first use. `GARTH_HOME` / `--garth-home` still work as deprecated aliases when you need to keep an older path explicitly.
107
+
108
+ ## Output Formats
109
+
110
+ All commands default to table output. Use `--json` or `--format csv` to change:
111
+
112
+ ```bash
113
+ garmin-cli health sleep --days 7 # table (default)
114
+ garmin-cli --json health sleep --days 7 # JSON envelope
115
+ garmin-cli --format csv health sleep --days 7 # CSV
116
+ ```
117
+
118
+ **JSON envelope structure:**
119
+
120
+ ```json
121
+ {
122
+ "ok": true,
123
+ "command": "health sleep",
124
+ "date_range": {"from": "2026-03-05", "to": "2026-03-11"},
125
+ "count": 7,
126
+ "data": [
127
+ {"date": "2026-03-05", "bedtime": "2026-03-04T23:10:00", "wake_time": "2026-03-05T06:25:00", "duration_hours": 7.2, "score": 78, ...}
128
+ ]
129
+ }
130
+ ```
131
+
132
+ **Error envelope:**
133
+
134
+ ```json
135
+ {
136
+ "ok": false,
137
+ "command": "health sleep",
138
+ "error": "Rate limited by Garmin. Try again later.",
139
+ "error_code": "RATE_LIMITED"
140
+ }
141
+ ```
142
+
143
+ Exit code is always `0` on success, `1` on error.
144
+
145
+ ## Commands
146
+
147
+ ### Health
148
+
149
+ ```bash
150
+ garmin-cli health sleep [--date DATE | --from DATE --to DATE | --days N]
151
+ garmin-cli health hrv [--date DATE | --from DATE --to DATE | --days N]
152
+ garmin-cli health weight [--date DATE | --from DATE --to DATE | --days N]
153
+ garmin-cli health body-battery [--date DATE | --from DATE --to DATE | --days N]
154
+ garmin-cli health stress [--date DATE | --from DATE --to DATE | --days N]
155
+ garmin-cli health spo2 [--date DATE | --from DATE --to DATE | --days N]
156
+ garmin-cli health resting-hr [--date DATE | --from DATE --to DATE | --days N]
157
+ garmin-cli health readiness [--date DATE | --from DATE --to DATE | --days N]
158
+ garmin-cli health status [--date DATE]
159
+ garmin-cli health steps [--date DATE | --from DATE --to DATE | --days N]
160
+ garmin-cli health daily-summary [--date DATE | --from DATE --to DATE | --days N]
161
+ garmin-cli health intensity-minutes [--date DATE | --from DATE --to DATE | --days N]
162
+ ```
163
+
164
+ `health daily-summary` makes one API call per day — large date ranges may be slow.
165
+
166
+ `health status` returns Garmin's full training-load picture for the day: training status, acute/chronic load, acute:chronic workload ratio (ACWR), the chronic-load "tunnel", monthly load-focus buckets with their targets, and load-balance status.
167
+
168
+ ### Activities
169
+
170
+ ```bash
171
+ garmin-cli activity list [--limit N] [--type TYPE] [--search TEXT] [--date DATE | --from DATE --to DATE | --days N]
172
+ garmin-cli activity get ACTIVITY_ID [--detail] [--laps] # --detail/-d shows sport-aware metrics; --laps appends lap data
173
+ garmin-cli activity laps ACTIVITY_ID # per-lap rows (run/bike: HR, power, cadence, start_time_gmt/local) or per-pool-length rows (lap_swimming)
174
+ garmin-cli activity zones ACTIVITY_ID # HR time-in-zone breakdown
175
+ garmin-cli activity weather ACTIVITY_ID # temperature, apparent temp, dew point, humidity, wind, condition
176
+ garmin-cli activity metrics-describe ACTIVITY_ID # metric descriptors: key, unit, metricsIndex
177
+ garmin-cli activity detail-metrics ACTIVITY_ID [--metric KEY]... # raw per-sample time series (one row per sample)
178
+ garmin-cli activity download ACTIVITY_ID [--fmt original|tcx|gpx|kml|csv] [--output PATH] [--force]
179
+ garmin-cli activity upload FILE # .fit / .gpx / .tcx
180
+ garmin-cli activity delete ACTIVITY_ID [--confirm]
181
+ garmin-cli activity rename ACTIVITY_ID NEW_NAME
182
+ garmin-cli activity set-type ACTIVITY_ID TYPE_KEY # e.g. running, cycling
183
+ ```
184
+
185
+ `--limit` defaults to 20, max 100. `--type` filters by activity type key (e.g., `running`, `cycling`).
186
+
187
+ `activity detail-metrics` returns the raw recorded sample stream (~2000 rows for a typical activity; columns are the watch's metric keys such as `directTimestamp`, `directHeartRate`, `directPower`). Use repeatable `--metric` flags to select columns — `activity metrics-describe` lists what a given activity recorded. This is the data for intra-activity analyses such as first-half vs second-half aerobic decoupling.
188
+
189
+ `activity download` writes the activity file to disk (it never prints binary to stdout). `--fmt` defaults to `original` (the FIT file inside a ZIP archive); the default output name is `activity_<id><ext>` in the current directory, and an existing file is not overwritten unless `--force` is given. `activity delete` prompts for confirmation unless `--confirm` is passed. `activity set-type` accepts any `typeKey` from Garmin's live sport-type table (an unknown key is rejected before any write).
190
+
191
+ #### Detailed sport-specific metrics
192
+
193
+ `activity get --detail` projects metrics scoped by the activity's sport:
194
+
195
+ | Sport | Detail-mode metrics (in addition to base summary, HR, calories, elevation, speed, elapsed time) |
196
+ |-------|-----------------------------------------------------------------------------------|
197
+ | Cycling | avg/max/normalized power, cadence (rpm), TSS, intensity factor, training effect, vO2max, recovery time |
198
+ | Running | cadence (spm), ground contact time, vertical oscillation/ratio, stride length, training effect, vO2max, recovery time |
199
+ | Lap swimming | SWOLF, total strokes, average stroke rate, distance per stroke, training effect |
200
+ | Open water swimming | universal extras + training effect (no per-length stroke metrics) |
201
+
202
+ Every sport also carries the universal training-response fields: `training_effect_label` (Garmin's primary-benefit label such as `TEMPO`), `training_load` (per-activity EPOC-based load), and `workout_id` (the structured workout the activity executed, linking plan to execution). `training_load` also appears in `activity list` rows.
203
+
204
+ `duration_min` is moving time; `elapsed_time_min` is total wall-clock time, so `elapsed - moving` is the time stopped.
205
+
206
+ JSON and CSV output use a stable union schema — every key is present (with `null` for sport-inapplicable metrics) so downstream parsers see a stable shape. Table output is sport-aware: only sport-applicable columns appear, keeping tables dense.
207
+
208
+ When `--detail` is set, JSON also carries an `unavailable` array describing which registry-known metrics are not produced for this activity. Each entry has `field`, `reason` (`not_applicable_to_sport` or `absent_in_response`), and `leg_index` (set on multisport child contributions). Table output prints a single counts-only footnote; CSV output stays a flat tabular format and does not carry the manifest.
209
+
210
+ ```bash
211
+ # Cycling detail
212
+ garmin-cli activity get 12345678 --detail
213
+
214
+ # Running detail with HR zones
215
+ garmin-cli activity get 12345678 --detail
216
+ garmin-cli activity zones 12345678
217
+
218
+ # Pool-swim per-length rows (auto-routes to typed_splits)
219
+ garmin-cli activity laps 12345678
220
+
221
+ # Cycling detail + lap power suite in one envelope
222
+ garmin-cli --json activity get 12345678 --detail --laps
223
+ ```
224
+
225
+ ### Workouts
226
+
227
+ ```bash
228
+ # Read
229
+ garmin-cli workout list [--limit N]
230
+ garmin-cli workout get WORKOUT_ID
231
+ garmin-cli workout calendar [--from DATE --to DATE | --days N | --ahead N]
232
+
233
+ # Write
234
+ garmin-cli workout create FILE # JSON or YAML file
235
+ garmin-cli workout create --stdin # read JSON from stdin
236
+ garmin-cli workout update WORKOUT_ID FILE # partial update (only fields provided change)
237
+ garmin-cli workout delete WORKOUT_ID [--confirm] # --confirm skips interactive prompt
238
+ garmin-cli workout schedule WORKOUT_ID DATE # DATE = YYYY-MM-DD
239
+ garmin-cli workout unschedule SCHEDULE_ID [--confirm] # removes a calendar entry; SCHEDULE_ID is the workoutScheduleId returned by schedule
240
+ ```
241
+
242
+ `--ahead N` shows the next N days of planned workouts (future-facing). `--days N` shows past N days.
243
+
244
+ YAML input is supported out of the box. See [SKILL.md](SKILL.md) for the full workout JSON schema reference and step/target types.
245
+
246
+ ### Performance
247
+
248
+ ```bash
249
+ garmin-cli performance thresholds
250
+ garmin-cli performance zones
251
+ garmin-cli performance vo2max
252
+ garmin-cli performance race-predictions
253
+ garmin-cli performance endurance-score [--date DATE | --from DATE --to DATE | --days N]
254
+ garmin-cli performance hill-score [--date DATE | --from DATE --to DATE | --days N]
255
+ garmin-cli performance personal-records
256
+ ```
257
+
258
+ `performance endurance-score` and `performance hill-score` make one API call per day — large date ranges may be slow.
259
+
260
+ `performance personal-records` returns all-time PRs (fastest 1 km through marathon, longest run/ride, biggest climb, best 20-min power, step records) with a human-readable `label`; typeIds Garmin has not documented publicly carry `label: null` with the raw value.
261
+
262
+ ### Devices
263
+
264
+ ```bash
265
+ garmin-cli device list # registered devices: device_id, display_name, device_type, last_sync_time
266
+ ```
267
+
268
+ ## Normalized JSON Schemas
269
+
270
+ Recent fixes normalized several JSON payloads for agent-safe output:
271
+
272
+ - `performance vo2max` returns `date`, `vo2max`, `sport`
273
+ - `performance zones` returns `sport`, `lt_hr_bpm`, `lt_pace`
274
+ - `workout calendar` includes `id`
275
+ - `workout get` includes `steps` and `steps_summary`
276
+ - `health hrv` reads `lastNightAvg` and still falls back to legacy `lastNight`
277
+
278
+ ## Date Range Options
279
+
280
+ | Option | Meaning | Example |
281
+ |--------|---------|---------|
282
+ | `--date DATE` | Single day | `--date 2026-03-11` |
283
+ | `--days N` | Past N days (inclusive) | `--days 7` |
284
+ | `--from DATE --to DATE` | Explicit range (both inclusive) | `--from 2026-03-01 --to 2026-03-11` |
285
+ | `--ahead N` | Next N days (calendar only) | `--ahead 7` |
286
+
287
+ Maximum range: 90 days. Conflicting options (e.g., `--date` with `--days`) produce a clear error.
288
+
289
+ ## Examples
290
+
291
+ ```bash
292
+ # Last week of sleep data
293
+ garmin-cli health sleep --days 7
294
+
295
+ # HRV for a specific date range
296
+ garmin-cli health hrv --from 2026-03-01 --to 2026-03-10
297
+
298
+ # List recent running activities as CSV
299
+ garmin-cli --format csv activity list --limit 10 --type running
300
+
301
+ # Activities from a specific date range
302
+ garmin-cli activity list --from 2026-03-01 --to 2026-03-31
303
+
304
+ # Activities from the past 7 days
305
+ garmin-cli activity list --days 7
306
+
307
+ # Upcoming planned workouts
308
+ garmin-cli workout calendar --ahead 7
309
+
310
+ # Create a workout from a JSON file
311
+ garmin-cli --json workout create my_workout.json
312
+
313
+ # Create and schedule a workout via stdin
314
+ echo '{"name":"Easy Run","sport":"running","steps":[{"type":"interval","duration":{"type":"time","value":1800},"target":{"type":"heart.rate.zone","zone":2}}]}' \
315
+ | garmin-cli --json workout create --stdin
316
+
317
+ # Schedule an existing workout
318
+ garmin-cli --json workout schedule 12345678901 2026-04-01
319
+
320
+ # All performance thresholds
321
+ garmin-cli performance thresholds
322
+
323
+ # Agent use: JSON output for scripting
324
+ garmin-cli --json health hrv --date 2026-03-11
325
+ garmin-cli --json activity list --limit 5
326
+ ```
327
+
328
+ ## Error Codes
329
+
330
+ | Code | Meaning |
331
+ |------|---------|
332
+ | `AUTH_MISSING` | No credentials found (no session and no env vars) |
333
+ | `AUTH_FAILED` | Credentials rejected by Garmin |
334
+ | `NOT_FOUND` | API endpoint unavailable (404) |
335
+ | `RATE_LIMITED` | 429 after 3 retries |
336
+ | `SERVER_ERROR` | 5xx after 3 retries |
337
+ | `NETWORK_ERROR` | Connection or timeout failure |
338
+ | `INVALID_INPUT` | Bad arguments or conflicting options |
339
+ | `INTERNAL_ERROR` | Unexpected error |
340
+
341
+ ## MCP Server (Optional)
342
+
343
+ Expose garmin-cli as an MCP tool server for local or remote MCP clients. Includes read tools for health, activities, workouts, performance, and devices, plus write tools for workouts (`workout_create`, `workout_schedule`, `workout_update`, `workout_delete`, `workout_unschedule`, with dry-run preview on create and update) and activities (`activity_download`, `activity_upload`, `activity_delete`, `activity_rename`, `activity_set_type`). The `report_snapshot` tool assembles a full morning/evening/weekly report in a single call, fanning out the underlying reads server-side — designed for recurring agent-driven daily summaries. See [SKILL.md](SKILL.md#report_snapshot-section-composition) for its section composition.
344
+
345
+ ### Installation
346
+
347
+ The package is published on PyPI as `garmin-py` (the CLI command remains `garmin-cli`). The recommended install method is `uv tool install`, which places the binary in `~/.local/bin` — a stable, venv-independent location that desktop applications can access without macOS sandbox issues:
348
+
349
+ ```bash
350
+ uv tool install "garmin-py[mcp]"
351
+ ```
352
+
353
+ Or from a source checkout:
354
+
355
+ ```bash
356
+ uv tool install --editable "/path/to/garmin-py[mcp]"
357
+ ```
358
+
359
+ To uninstall:
360
+
361
+ ```bash
362
+ uv tool uninstall garmin-py
363
+ ```
364
+
365
+ **Avoid pointing MCP clients at a binary inside a project virtualenv** (e.g. `.venv/bin/garmin-cli`). On macOS, desktop applications run in a sandbox and cannot read `pyvenv.cfg` inside directories they have not been granted access to, which causes a fatal Python startup error:
366
+
367
+ ```
368
+ PermissionError: [Errno 1] Operation not permitted: '/path/to/.venv/pyvenv.cfg'
369
+ ```
370
+
371
+ The `uv tool install` approach avoids this entirely. Alternatively, grant Claude Desktop Full Disk Access in System Settings → Privacy & Security → Full Disk Access.
372
+
373
+ ### Claude Desktop
374
+
375
+ Add to your Claude Desktop config file:
376
+
377
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
378
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
379
+
380
+ ```json
381
+ {
382
+ "mcpServers": {
383
+ "garmin": {
384
+ "command": "/Users/YOU/.local/bin/garmin-cli",
385
+ "args": ["mcp-server"]
386
+ }
387
+ }
388
+ }
389
+ ```
390
+
391
+ With a custom session directory:
392
+
393
+ ```json
394
+ {
395
+ "mcpServers": {
396
+ "garmin": {
397
+ "command": "/Users/YOU/.local/bin/garmin-cli",
398
+ "args": ["--garmin-home", "/path/to/.garminconnect", "mcp-server"]
399
+ }
400
+ }
401
+ }
402
+ ```
403
+
404
+ ### Claude Code
405
+
406
+ ```bash
407
+ claude mcp add --transport stdio garmin -- garmin-cli mcp-server
408
+ ```
409
+
410
+ ### ChatGPT (via MCP bridge)
411
+
412
+ ChatGPT does not natively support MCP. To connect, run the server with an HTTP transport and use an MCP-to-OpenAI bridge such as [mcp-openai-bridge](https://github.com/nicobailey/mcp-openai-bridge) or a similar proxy:
413
+
414
+ ```bash
415
+ # Start the MCP server with streamable HTTP
416
+ garmin-cli mcp-server --transport streamable-http --host 127.0.0.1 --port 8000
417
+ ```
418
+
419
+ Then point the bridge at `http://127.0.0.1:8000/mcp` and configure it as a ChatGPT plugin or custom GPT action. Refer to the bridge project's documentation for setup details.
420
+
421
+ ### HTTP Transports
422
+
423
+ SSE and streamable HTTP use the MCP SDK's built-in HTTP server. `--host` defaults to `127.0.0.1` (loopback only).
424
+
425
+ Streamable HTTP (recommended for remote clients):
426
+
427
+ ```bash
428
+ garmin-cli mcp-server --transport streamable-http --port 8000
429
+ ```
430
+
431
+ SSE (for clients that require it):
432
+
433
+ ```bash
434
+ garmin-cli mcp-server --transport sse --port 8000
435
+ ```
436
+
437
+ Optional HTTP flags: `--sse-path`, `--message-path` (SSE only), `--streamable-http-path`, `--stateless-http`, `--json-response` (streamable-http only).
438
+
439
+ #### Bearer-token gate on non-loopback binds
440
+
441
+ Binding to any non-loopback address (`--host 0.0.0.0` or an external interface) requires a bearer token. The server refuses to start otherwise.
442
+
443
+ ```bash
444
+ export GARMIN_MCP_BEARER_TOKEN="<a-long-random-token>"
445
+ garmin-cli mcp-server --transport streamable-http --host 0.0.0.0 --port 8000
446
+ ```
447
+
448
+ When the token is set and the bind is non-loopback, the MCP SDK gates every tool call (read **and** write) through `Authorization: Bearer <token>` at the transport layer. Loopback binds (`127.0.0.1`, `::1`, `localhost`) and the `stdio` transport are not gated -- they are trusted to the same degree as the shell user running the process.
449
+
450
+ TLS is expected to be terminated by a reverse proxy in front of the server; the built-in HTTP listener is plain HTTP.
451
+
452
+ For remote clients, prefer a dedicated session directory with `--garmin-home` rather than exporting credentials into another process.
453
+
454
+ See [SKILL.md](SKILL.md#mcp-server-alternative) for the full tool list and parameter reference.
455
+
456
+ ## Development
457
+
458
+ ```bash
459
+ pip install -e ".[dev]"
460
+ pytest tests/ # unit tests (1100+ tests)
461
+ pytest tests/ --e2e # unit + e2e tests (requires GARMIN_HOME/garmin_tokens.json)
462
+ ```
463
+
464
+ To run MCP server tests, also install the mcp extra:
465
+
466
+ ```bash
467
+ pip install -e ".[dev,mcp]"
468
+ ```
469
+
470
+ E2E tests make real Garmin Connect API calls. They require a valid `garmin_tokens.json` session in `~/.garminconnect` (or `GARMIN_HOME`; `GARTH_HOME` still works as an alias). Set `E2E_RATE_LIMIT_SECONDS` (default: 5) to adjust the inter-request delay.