Metadata-Version: 2.1
Name: garminconnect
Version: 0.3.9
Summary: Python 3 API wrapper for Garmin Connect
Keywords: garmin connect,api,garmin
Author-Email: Ron Klinkien <ron@cyberjunky.nl>
License: MIT
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: OS Independent
Project-URL: Homepage, https://github.com/cyberjunky/python-garminconnect
Project-URL: Issues, https://github.com/cyberjunky/python-garminconnect/issues
Project-URL: Changelog, https://github.com/cyberjunky/python-garminconnect/releases
Requires-Python: >=3.12
Requires-Dist: curl_cffi>=0.15.0
Requires-Dist: requests>=2.33.0
Requires-Dist: ua-generator>=1.0
Provides-Extra: dev
Requires-Dist: ipython; extra == "dev"
Requires-Dist: ipdb; extra == "dev"
Requires-Dist: ipykernel; extra == "dev"
Requires-Dist: pandas; extra == "dev"
Requires-Dist: matplotlib; extra == "dev"
Provides-Extra: workout
Requires-Dist: pydantic>=2.4.0; extra == "workout"
Provides-Extra: typed
Requires-Dist: pydantic>=2.4.0; extra == "typed"
Provides-Extra: security
Requires-Dist: bandit[toml]; extra == "security"
Requires-Dist: pip-audit; extra == "security"
Provides-Extra: linting
Requires-Dist: black[jupyter]; extra == "linting"
Requires-Dist: ruff; extra == "linting"
Requires-Dist: mypy; extra == "linting"
Requires-Dist: isort; extra == "linting"
Requires-Dist: types-requests; extra == "linting"
Requires-Dist: pre-commit; extra == "linting"
Requires-Dist: codespell; extra == "linting"
Provides-Extra: testing
Requires-Dist: coverage; extra == "testing"
Requires-Dist: pytest; extra == "testing"
Requires-Dist: pytest-vcr>=1.0.2; extra == "testing"
Requires-Dist: vcrpy>=7.0.0; extra == "testing"
Provides-Extra: example
Requires-Dist: readchar; extra == "example"
Description-Content-Type: text/markdown

[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]](LICENSE)
![Project Maintenance][maintenance-shield]

[![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/)
[![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky)

# Python: Garmin Connect

The Garmin Connect API library comes with two examples:

- **`example.py`** - Simple getting-started example showing authentication, token storage, and basic API calls
- **`demo.py`** - Comprehensive demo providing access to **130+ API methods** organized into **13 categories** for easy navigation

```bash
$ ./demo.py
🏃‍♂️ Full-blown Garmin Connect API Demo - Main Menu
==================================================
Select a category:

  [1] 👤 User & Profile
  [2] 📊 Daily Health & Activity
  [3] 🔬 Advanced Health Metrics
  [4] 📈 Historical Data & Trends
  [5] 🏃 Activities & Workouts
  [6] ⚖️ Body Composition & Weight
  [7] 🏆 Goals & Achievements
  [8] ⌚ Device & Technical
  [9] 🎽 Gear & Equipment
  [0] 💧 Hydration & Wellness
  [a] 🔧 System & Export
  [b] 📅 Training plans
  [c] ⛳ Golf

  [q] Exit program

Make your selection:
```

## API Coverage Statistics

- **Total API Methods**: 143+ unique endpoints (snapshot)
- **Categories**: 13 organized sections
- **User & Profile**: 4 methods (basic user info, settings)
- **Daily Health & Activity**: 10 methods (today's health data plus daily calories, resting HR and sleep ranges)
- **Advanced Health Metrics**: 16 methods (fitness metrics, HRV, VO2, FTP range, training readiness, training zones, running tolerance)
- **Historical Data & Trends**: 9 methods (date range queries, weekly aggregates)
- **Activities & Workouts**: 41 methods (comprehensive activity, workout management, typed workout uploads including strength, in-place edit, scheduling, push to device, import, edit description / exercise sets)
- **Body Composition & Weight**: 8 methods (weight tracking, body composition)
- **Goals & Achievements**: 15 methods (challenges, badges, goals)
- **Device & Technical**: 7 methods (device info, settings)
- **Gear & Equipment**: 7 methods (gear management, tracking)
- **Hydration & Wellness**: 12 methods (hydration, nutrition, blood pressure, menstrual)
- **System & Export**: 4 methods (reporting, logout, GraphQL)
- **Training Plans**: 3 methods (plans, plan by ID, adaptive plan by ID)
- **Golf**: 5 methods (scorecard summary, scorecard detail, shot data, club stats, user stats)

### Interactive Features

- **Enhanced User Experience**: Categorized navigation with emoji indicators
- **Smart Data Management**: Interactive weigh-in deletion with search capabilities
- **Comprehensive Coverage**: All major Garmin Connect features are accessible
- **Error Handling**: Robust error handling with user-friendly prompts
- **Data Export**: JSON export functionality for all data types

[![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/)
[![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky)

A comprehensive Python3 API wrapper for Garmin Connect, providing access to health, fitness, and device data.

## 📖 About

This library enables developers to programmatically access Garmin Connect data including:

- **Health Metrics**: Heart rate, sleep, stress, body composition, SpO2, HRV
- **Activity Data**: Workouts, typed workout uploads (running, cycling, swimming, walking, hiking, strength), workout scheduling, exercises, training status, performance metrics, import-style uploads (no Strava re-export)
- **Nutrition**: Daily food logs, meals, and nutrition settings
- **Golf**: Scorecard summaries, scorecard details, shot-by-shot data
- **Device Information**: Connected devices, settings, alarms, solar data
- **Goals & Achievements**: Personal records, badges, challenges, race predictions
- **Historical Data**: Trends, progress tracking, date range queries

Compatible with all Garmin Connect accounts. See <https://connect.garmin.com/>

## 📦 Installation

Requires Python 3.12 or later.

Install from PyPI:

```bash
pip install --upgrade garminconnect curl_cffi
```

## Run demo software (recommended)

Clone the repo, then:

```bash
python3 -m venv .venv --copies
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e ".[example]"

python3 ./example.py   # simple getting-started example
python3 ./demo.py      # comprehensive demo (130+ API methods)
```

## 🛠️ Development

This project uses [PDM](https://pdm.fming.dev/) for dependency management and task automation.

> **⚠️ Important**: Create a virtual environment first on externally-managed Python installs (Debian/Ubuntu) to avoid system package conflicts.

```bash
python3 -m venv .venv --copies
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install pdm
python3 -m pdm install --group :all
pre-commit install --install-hooks  # optional but recommended
```

> **Note**: Using `python -m pdm` instead of `pdm` avoids PATH issues on some
> Windows setups where `pip install pdm` places the `pdm` executable outside
> the directories on `PATH`. Once `pdm install` has run, subsequent `pdm run ...`
> commands work normally because the venv's `Scripts/` directory is on `PATH`
> while the venv is active.

**Development commands:**

```bash
pdm run format      # Auto-format code (isort, black, ruff --fix)
pdm run lint        # Check code quality (isort, ruff, black, mypy)
pdm run codespell   # Check spelling
pdm run test        # Run test suite
pdm run testcov     # Run tests with coverage report
pdm run all         # Run all checks (lint + codespell + pre-commit + test)
pdm run clean       # Clean build artifacts and cache files
pdm run build       # Build package for distribution
pdm run publish     # Build and publish to PyPI
pdm run --list      # Show all available commands
```

Run `pdm run format && pdm run lint && pdm run test` before submitting PRs.

## 🔐 Authentication

Authentication uses the same mobile SSO flow as the official Garmin Connect Android app.
No browser is needed.

**How it works:**

1. **First login**: Authenticates via `sso.garmin.com/mobile/api/login` using the Android
   app's client ID. If MFA is required, a callback (`prompt_mfa`) prompts for the one-time code.
2. **Token exchange**: The service ticket is exchanged for DI OAuth Bearer tokens
   (`access_token` + `refresh_token`) via `diauth.garmin.com`. Tokens are stored at
   `~/.garminconnect/garmin_tokens.json`.
3. **Auto-refresh**: Before each API request the library checks whether the DI token is about
   to expire and refreshes it automatically — no user interaction required.

**Session lifetime:**
- DI tokens auto-refresh indefinitely as long as the refresh token remains valid.
- A full re-login with credentials (and possibly MFA) is only needed if the refresh token
  itself expires or is revoked.

**Token storage:**
```bash
~/.garminconnect/garmin_tokens.json   # saved automatically, mode 0600
```

The containing directory is restricted to mode `0700`. Treat the token file
like a password: the refresh token can provide persistent account access. Avoid
putting a Garmin password in shell history or a long-lived environment variable;
prefer `getpass()` or another interactive secret prompt.

**Resilient login (multi-strategy + token validation):**

`login()` tries several authentication strategies in order (mobile, SSO widget,
web portal — each with and without TLS impersonation) and only declares success
when the resulting token is actually accepted by the API. If a strategy obtains
a token the API later rejects (a region/account-specific condition — see
[#369](https://github.com/cyberjunky/python-garminconnect/issues/369)), the
library transparently falls through to the next strategy. Set
`Garmin(..., verify_login=False)` to restore the legacy "first token wins"
behavior.

**Cached-token gotcha & self-healing:** when a `tokenstore` is supplied,
`login()` loads those tokens *before* the strategy chain and short-circuits if
they load — so stale/poisoned cached tokens used to fail every run. The library
now detects this: if cached tokens are rejected by the API, it discards them and
performs a fresh credential login automatically. To force a clean slate yourself
(e.g. between a failed resume and a retry), call:

```python
g.logout()  # clears in-memory auth + cached tokens (uses GARMINTOKENS)
g.logout(tokenstore)  # or pass an explicit path
```

`logout()` removes only the local `garmin_tokens.json` file and preserves its
directory and unrelated files. It does **not** revoke a token that has already
been issued by Garmin. Revoke account access from Garmin's account/security
settings if a token may have been copied or exposed.

### What running it locally does

This is an unofficial client for Garmin's web services; it does not pair with
the Garmin Connect phone app or connect directly to a watch. When you call
`login()`, your credentials and MFA code are sent over HTTPS to Garmin's login
service. The library receives an access/refresh token pair and, when a token
store is supplied, caches it locally for later sessions. Subsequent API methods
send that token to Garmin and can read or change the same account data that the
selected method targets.

The library does not automatically download an entire account. The demo writes
responses, activity downloads, and health reports only when you select those
actions. Demo exports are stored under `your_data/` with owner-only directory
and file permissions. Run the project in a dedicated virtual environment, read
the method you plan to call, and start with read-only methods. Upload, edit,
delete, schedule, hydration, and weigh-in methods can change Garmin account data.

## 🧪 Testing

The default suite is credential-free and excludes live-account integration tests:

```bash
pdm run test        # Run all tests
pdm run testcov     # Run tests with coverage report
```

To explicitly run live integration tests, use a test account if possible. This
can create local VCR recordings and includes methods that may mutate the account:

```bash
export GARMIN_EMAIL="you@example.com"
read -s GARMIN_PASSWORD && export GARMIN_PASSWORD
pdm run pytest -m integration --vcr-record=once
unset GARMIN_PASSWORD
```

VCR recordings are ignored by Git because Garmin responses contain sensitive
health, activity, location, and account data. Do not commit them.

## 📦 Publishing

For package maintainers:

**Setup PyPI credentials:**

```bash
pip install twine
# Edit with your preferred editor, or create via here-doc:
# cat > ~/.pypirc <<'EOF'
# [pypi]
# username = __token__
# password = <PyPI_API_TOKEN>
# EOF
```

```ini
[pypi]
username = __token__
password = <PyPI_API_TOKEN>
```

Recommended: use environment variables and restrict file perms

```bash
chmod 600 ~/.pypirc
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="<PyPI_API_TOKEN>"
```

**Publish new version:**

```bash
pdm run publish    # Build and publish to PyPI
```

**Alternative publishing steps:**

```bash
pdm run build      # Build package only
pdm publish        # Publish pre-built package
```

## 🤝 Contributing

We welcome contributions! Here's how you can help:

- **Report Issues**: Bug reports and feature requests via GitHub issues
- **Submit PRs**: Code improvements, new features, documentation updates
- **Testing**: Help test new features and report compatibility issues
- **Documentation**: Improve examples, add use cases, fix typos

**Before contributing:**
1. Set up your dev environment (see [Development](#️-development) above)
2. Format and lint: `pdm run format && pdm run lint`
3. Run tests: `pdm run test`
4. Follow existing code style and patterns

### Jupyter Notebook

Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/docs/reference.ipynb).

### Python Code Examples

```python
import os
from getpass import getpass
from datetime import date
from garminconnect import Garmin

# First run: logs in and saves tokens to ~/.garminconnect
# Subsequent runs: loads saved tokens and auto-refreshes
client = Garmin(
    os.getenv("GARMIN_EMAIL"),
    getpass("Garmin password: "),
    prompt_mfa=lambda: input("MFA code: "),
)
client.login("~/.garminconnect")

# Get today's stats
today = date.today().isoformat()
stats = client.get_stats(today)

# Get heart rate data
hr_data = client.get_heart_rates(today)
print(f"Resting HR: {hr_data.get('restingHeartRate', 'n/a')}")
```

### Typed Workouts (Pydantic Models)

The library includes optional typed workout models for creating type-safe workout definitions:

```bash
pip install garminconnect[workout]
```

```python
from garminconnect.workout import (
    RunningWorkout,
    WorkoutSegment,
    create_warmup_step,
    create_interval_step,
    create_distance_interval_step,
    create_cooldown_step,
    create_repeat_group,
)

# Create a structured running workout
workout = RunningWorkout(
    workoutName="Easy Run",
    estimatedDurationInSecs=1800,
    workoutSegments=[
        WorkoutSegment(
            segmentOrder=1,
            sportType={"sportTypeId": 1, "sportTypeKey": "running"},
            workoutSteps=[create_warmup_step(300.0)],
        )
    ],
)

# Upload and optionally schedule it
result = client.upload_running_workout(workout)
client.schedule_workout(result["workoutId"], "2026-03-20")

# Edit it in place - keeps its id, so any schedules pointing at it stay valid
workout_data = client.get_workout_by_id(result["workoutId"])
workout_data["workoutName"] = "Easy Run (revised)"
client.update_workout(result["workoutId"], workout_data)

# Delete a workout or remove it from the calendar
client.delete_workout(workout_id)
client.unschedule_workout(scheduled_workout_id)

# Push a workout to a device - defaults to the last workout / last used device
client.push_workout_to_device(result["workoutId"], device_id)
```

**Available workout classes:** `RunningWorkout`, `CyclingWorkout`, `SwimmingWorkout`, `WalkingWorkout`, `HikingWorkout`, `StrengthWorkout`, `MultiSportWorkout`, `FitnessEquipmentWorkout`

**Strength workouts** are rep-based. Build each exercise with `create_strength_set(category, step_order, sets, reps, rest_seconds, exercise_name="", weight_kg=None)` and identify exercises with a `category` / `exercise` pair from the bundled catalog in `garminconnect.exercises` (1,527 exercises across 47 categories, with `resolve(name)` and `find(term)` helpers):

```python
from garminconnect import exercises
from garminconnect.workout import StrengthWorkout, WorkoutSegment, create_strength_set

lat = exercises.resolve(
    "Lat Pull-down"
)  # {'category': 'PULL_UP', 'exercise': 'LAT_PULLDOWN'}

workout = StrengthWorkout(
    workoutName="Upper Body",
    estimatedDurationInSecs=0,
    workoutSegments=[
        WorkoutSegment(
            segmentOrder=1,
            sportType={"sportTypeId": 5, "sportTypeKey": "strength_training"},
            workoutSteps=[
                create_strength_set(
                    "BENCH_PRESS", step_order=1, sets=4, reps=10, rest_seconds=120
                ),
                create_strength_set(
                    lat["category"],
                    step_order=4,
                    sets=3,
                    reps=12,
                    rest_seconds=90,
                    exercise_name=lat["exercise"],
                ),
            ],
        )
    ],
)
client.upload_strength_workout(workout)
```

**Helper functions:** `create_warmup_step`, `create_interval_step`, `create_distance_interval_step`, `create_recovery_step`, `create_cooldown_step`, `create_repeat_group`, `create_strength_exercise_step`, `create_strength_rest_step`, `create_strength_set`

Use `create_distance_interval_step(600.0, step_order=1)` for interval steps that should end after a distance in meters instead of after a duration.

### Golf

The library supports reading Garmin Golf scorecards, shot-level data, club statistics, and golf user statistics.

```python
# List recent golf scorecards
summary = client.get_golf_summary(limit=10)
scorecard_id = summary["scorecardSummaries"][0]["id"]

# Get the full scorecard details
scorecard = client.get_golf_scorecard(scorecard_id)

# Get shot-by-shot data
# - Omit hole_numbers to receive every hole (recommended for holes 10-18).
# - Single-digit holes work as a comma- or dash-separated list, e.g. "1,2,3".
#   Commas are normalized to dashes because Garmin only accepts '-' as the
#   separator.
# - Double-digit holes (10-18) cannot be fetched with a filter at all; the
#   API either drops them or returns an empty response. If you request holes
#   10-18, the library falls back to returning all 18 holes.
shots = client.get_golf_shot_data(scorecard_id)
shots = client.get_golf_shot_data(scorecard_id, hole_numbers="1,2,3")

# Club and player statistics
club_stats = client.get_golf_club_stats()
user_stats = client.get_golf_user_stats()
```

### Additional Resources
- **Simple Example**: [example.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/example.py) - Getting started guide
- **Comprehensive Demo**: [demo.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/demo.py) - All 130+ API methods
- **API Documentation**: Comprehensive method documentation in source code
- **Test Cases**: Real-world usage examples in `tests/` directory

## 🙏 Acknowledgments

Special thanks to all contributors who have helped improve this project:

- **Community Contributors**: Bug reports, feature requests, and code improvements
- **Issue Reporters**: Helping identify and resolve compatibility issues
- **Feature Developers**: Adding new API endpoints and functionality
- **Documentation Authors**: Improving examples and user guides

This project thrives thanks to community involvement and feedback.

## 💖 Support This Project

If you find this library useful for your projects, please consider supporting its continued development and maintenance:

### 🌟 Ways to Support

- **⭐ Star this repository** - Help others discover the project
- **💰 Financial Support** - Contribute to development and hosting costs
- **🐛 Report Issues** - Help improve stability and compatibility
- **📖 Spread the Word** - Share with other developers

### 💳 Financial Support Options

[![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/)
[![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky)

**Why Support?**
- Keeps the project actively maintained
- Enables faster bug fixes and new features
- Supports infrastructure costs (testing, AI, CI/CD)
- Shows appreciation for hundreds of hours of development

Every contribution, no matter the size, makes a difference and is greatly appreciated! 🙏

[releases-shield]: https://img.shields.io/github/release/cyberjunky/python-garminconnect.svg?style=for-the-badge
[releases]: https://github.com/cyberjunky/python-garminconnect/releases
[commits-shield]: https://img.shields.io/github/commit-activity/y/cyberjunky/python-garminconnect.svg?style=for-the-badge
[commits]: https://github.com/cyberjunky/python-garminconnect/commits/master
[license-shield]: https://img.shields.io/github/license/cyberjunky/python-garminconnect.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-cyberjunky-blue.svg?style=for-the-badge
