Skip to content

Conversation

@cyberjunky
Copy link
Owner

@cyberjunky cyberjunky commented Aug 31, 2025

Better error catching and typing in API code
Very enhancements made to example.py demonstrating all current API calls and possibilities this package can offer
Provided more sample data and upload demonstration
Health report example
Merged track gear usage example with example.py
Several development environment enhancements

Summary by CodeRabbit

  • New Features

    • GraphQL query support, workout upload, and a streamlined runnable example + demo enhancements.
  • Bug Fixes

    • Hardened login/token handling, improved error translation and input validation.
  • Documentation

    • Major README overhaul with examples, testing, publishing, interactive demo and API coverage stats.
  • Style

    • EditorConfig, pre-commit hooks (Black, Ruff, Mypy) and broad typing/formatting updates.
  • Tests

    • Added GPX/JSON fixtures, refreshed cassettes, stronger sanitization and more resilient tests.
  • Chores

    • CI trimmed, version/license bumped, packaging/tooling updates, removed legacy Makefile and deprecated example.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 31, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid enum value. Expected 'chill' | 'assertive', received 'pythonic' at "reviews.profile"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Adds EditorConfig and CI/tooling updates; strengthens library typing, validation, GraphQL and upload APIs; reworks example scripts; removes root Makefile and one example; adds GPX/JSON test data; updates many VCR cassettes; hardens tests and VCR sanitization; bumps packaging metadata.

Changes

Cohort / File(s) Change Summary
Repository config & automation
/.editorconfig, /.gitignore, /.coderabbit.yaml, /.github/dependabot.yml, /.pre-commit-config.yaml
Add EditorConfig; adjust .gitignore (add your_data/, pdm.lock, .vscode/; remove response.json ignore; un-ignore __pypackages__); change CodeRabbit profile to pythonic, enable request-changes and auto-fix; Dependabot schedule to daily (20:00 Europe/Amsterdam); add Black/Ruff/mypy pre-commit hooks and update pre-commit entries.
CI & workflows
/.github/workflows/ci.yml
Keep Python matrix (3.10–3.13); retain setup, lint/format/type checks (ruff/black/mypy); pytest and coverage-upload steps commented out; coverage artifact upload left conditional for py3.11.
Packaging & project metadata
/pyproject.toml, requirements-dev.txt, requirements-test.txt
Bump version to 0.2.30; tighten garth constraint; add pdm.backend build-backend; add/expand tool configs (pdm scripts, ruff, mypy, isort, coverage); move dev/test tooling to pyproject and clear legacy requirements files.
Docs, license & examples
/README.md, /LICENSE, /docs/reference.ipynb, example_tracking_gear.py
Major README rewrite (demo/menu/docs rework, badges); MIT year bumped to 2025; minor notebook quoting fix; delete example_tracking_gear.py.
Removed automation
/Makefile
Remove root Makefile and all defined development targets.
Core library: API, validation & public surface
garminconnect/__init__.py
Add constants (MAX_ACTIVITY_LIMIT, MAX_HYDRATION_ML, DATE_FORMAT_* , VALID_WEIGHT_UNITS), input validators and _fmt_ts; typed ctor/login/resume flows, tokenstore handling; GraphQL endpoint constant and query_garmin_graphql; upload_workout; map HTTP errors to new public exceptions (Authentication/TooManyRequests/Connection/InvalidFileFormat); broaden type hints and return types.
FIT encoder typing
garminconnect/fit.py
Add extensive Python 3.10+ type annotations and modernized signatures/returns across Fit encoder; preserve binary-specific mypy-ignore.
Examples & scripts
example.py
Replace large interactive demo with streamlined auth-first example: tokenstore-first login, MFA resume flow, safe_api_call, display_user_info/display_daily_stats, token persistence; change init_api() signature.
Tests: fixtures, sanitization & typing
tests/conftest.py, tests/test_garmin.py
Add type hints; introduce scrub_dates and stronger VCR sanitizers (mask tokens/cookies/headers, normalize dates); vcr fixture fallback to ~/.garminconnect; harden tests for 403/409 and variable payload shapes; update test typing and fixtures usage.
Test data & assets
test_data/sample_activity.gpx, test_data/sample_workout.json
Add GPX sample activity (per-point HR/time series) and serialized workout JSON for tests.
VCR cassettes & test fixtures (bulk)
tests/cassettes/...
Large cassette updates: many responses switched to user-settings / GUID-based paths, UA normalized to GCM-iOS-5.7.2.1, cookies simplified to _cfuvid, headers normalized (Content-Encoding/cf-cache-status), payload IDs/structures updated, some statuses changed (204, 409), and many user/profile field restructures.
IDE & misc
.vscode/settings.json, docs/*, tests/cassettes/*
Add trailing newline to VSCode settings; minor docs tweaks; broad cassette/header normalizations across fixtures.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User code
  participant G as garminconnect.Garmin
  participant FS as Filesystem
  participant API as Garmin Connect API

  rect rgba(200,220,255,0.08)
  note over U,G: Login flow (tokenstore preferred, credentials + MFA fallback)
  U->>G: login(tokenstore: Optional[str])
  alt tokenstore exists
    G->>FS: read tokenstore
    FS-->>G: tokens
    G->>API: validate token
    API-->>G: 200 / 401
  else credentials path
    U->>G: provide email/password
    G->>API: authenticate
    API-->>G: tokens / MFA challenge
    alt MFA required
      U->>G: resume_login(mfa_code)
      G->>API: resume auth
    end
  end
  alt auth success
    G-->>U: session/profile
  else auth failure
    G-->>U: raise GarminConnectAuthenticationError
  end
  end
Loading
sequenceDiagram
  autonumber
  participant U as User code
  participant G as garminconnect.Garmin
  participant API as Garmin Connect API

  rect rgba(255,240,200,0.08)
  note over U,G: upload_workout(payload) — validate, POST, map errors
  U->>G: upload_workout(payload)
  G->>G: validate payload (types, bounds)
  alt invalid
    G-->>U: raise GarminConnectInvalidFileFormatError or ValueError
  else valid
    G->>API: POST /upload or GraphQL
    API-->>G: 200/202 result / 409 / 429 / 401
    alt success
      G-->>U: return result dict
    else mapped error
      G-->>U: raise Authentication/TooManyRequests/Connection
    end
  end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

"I hopped through diffs with whiskered cheer,
Types and validators now bound and clear.
Cassettes refreshed, CI trimmed its load,
GPX and workouts tucked down the road.
A rabbit nods — the code sleeps snug tonight 🐇"

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch revamp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (13)
.gitignore (3)

7-7: Duplicate pypackages/ entry.

Remove one to reduce noise.

-__pypackages__/

Also applies to: 106-106


45-46: Consider adding .vscode/ for completeness.

Many devs use VS Code; ignoring its folder avoids accidental commits.

 # PyCharm idea folder
 .idea/
+# VS Code
+.vscode/

143-144: Minor: group custom ignores at top and dedupe.

Optional tidy-up to keep this file short and maintainable.

pyproject.toml (5)

41-41: Line-length mismatch (isort 79 vs Black/Ruff 88).

This will churn diffs. Align isort to 88 or rely on Ruff’s import sorter.

Option A (align isort):

 [tool.isort]
 ...
-line_length = 79
+line_length = 88

Option B (prefer Ruff, drop isort rules and dependency):

-[tool.isort]
-multi_line_output = 3
-include_trailing_comma = true
-force_grid_wrap = 0
-use_parentheses = true
-line_length = 79
-known_first_party = "garminconnect"

And remove "isort" from optional and dev linting groups.

Also applies to: 73-75


44-59: Duplication between optional-dependencies and dev-dependencies.

Define extras for users; keep dev groups for contributors. Ensure pre-commit lives in dev linting too.

 [project.optional-dependencies]
 linting = [
     "black",
     "ruff",
     "mypy",
-    "isort",
     "types-requests",
-    "pre-commit",
 ]
 
 [tool.pdm.dev-dependencies]
 linting = [
     "black",
     "ruff",
     "mypy",
-    "isort",
     "types-requests",
+    "pre-commit",
 ]

If you decide to keep isort, retain it in both places consistently and align config as noted above.

Also applies to: 117-131


30-35: Mypy config minimal.

Consider pinning python_version and enabling stricter checks incrementally.

 [tool.mypy]
 ignore_missing_imports = true
+python_version = "3.10"
+# enable gradually:
+# disallow_untyped_defs = true
+# warn_unused_ignores = true

21-21: keywords formatting minor nit.

PEP 621 examples show a list of strings; yours is fine—optionally split per-line for readability.


36-43: If keeping isort + Black, ensure compatible profile.

Prefer isort’s black profile to avoid churn.

 [tool.isort]
-multi_line_output = 3
-include_trailing_comma = true
-force_grid_wrap = 0
-use_parentheses = true
-line_length = 79
+profile = "black"
+line_length = 88
 known_first_party = "garminconnect"
garminconnect/__init__.py (5)

133-135: Duplicate assignment of garmin_connect_adhoc_challenges_url.

The URL is set twice to the same value; drop one.

-        self.garmin_connect_adhoc_challenges_url = (
-            "/adhocchallenge-service/adHocChallenge/historical"
-        )

1170-1183: Avoid KeyError on deviceSolarInput.

connectapi may return None or a dict without deviceSolarInput. Guard with .get and raise a clearer error.

-        return self.connectapi(url, params=params)["deviceSolarInput"]
+        resp = self.connectapi(url, params=params)
+        if not resp or "deviceSolarInput" not in resp:
+            raise GarminConnectConnectionError("No device solar input data received")
+        return resp["deviceSolarInput"]

1189-1196: Harden get_device_alarms against missing keys.

device_settings["alarms"] can KeyError. Use .get.

-            device_alarms = device_settings["alarms"]
+            device_alarms = device_settings.get("alarms")

1669-1680: Public API typo: get_gear_ativities.

Typo may propagate. Keep existing for backward compatibility, but add a correctly spelled alias.

# Outside of selected range; add near the method
def get_gear_activities(self, gearUUID, limit=9999):
    return self.get_gear_ativities(gearUUID, limit)

258-259: upload_activity: minor hardening and readability.

Use pathlib, and consider streaming upload via file handle to avoid reading whole file into memory.

-from os.path import exists, isfile, basename
+from pathlib import Path
@@
-        if not os.path.exists(activity_path):
+        p = Path(activity_path)
+        if not p.exists():
             raise FileNotFoundError(f"File not found: {activity_path}")
-        if not os.path.isfile(activity_path):
+        if not p.is_file():
             raise ValueError(f"Path is not a file: {activity_path}")
-        file_base_name = os.path.basename(activity_path)
+        file_base_name = p.name
@@
-                with open(activity_path, "rb") as file_handle:
-                    files = {
-                        "file": (file_base_name, file_handle.read()),
-                    }
+                with p.open("rb") as file_handle:
+                    files = {"file": (file_base_name, file_handle)}

Also applies to: 1326-1373

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0caa04a and 4b61aad.

📒 Files selected for processing (10)
  • .editorconfig (1 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • .gitignore (1 hunks)
  • LICENSE (1 hunks)
  • README.md (1 hunks)
  • example_tracking_gear.py (0 hunks)
  • garminconnect/__init__.py (32 hunks)
  • pyproject.toml (3 hunks)
  • test_data/sample_activity.gpx (1 hunks)
  • test_data/sample_workout.json (1 hunks)
💤 Files with no reviewable changes (1)
  • example_tracking_gear.py
🧰 Additional context used
🪛 actionlint (1.7.7)
.github/workflows/ci.yml

20-20: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


61-61: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 YAMLlint (1.37.1)
.github/workflows/ci.yml

[warning] 3-3: truthy value should be one of [false, true]

(truthy)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 7-7: too many spaces inside brackets

(brackets)


[error] 7-7: too many spaces inside brackets

(brackets)


[error] 17-17: wrong indentation: expected 6 but found 4

(indentation)


[error] 58-58: wrong indentation: expected 6 but found 4

(indentation)

🪛 LanguageTool
README.md

[grammar] ~3-~3: Use correct spacing
Context: ...o 11 categories for easy navigation: bash $ ./example.py 🏃‍♂️ 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 [q] Exit program Make your selection: ### API Coverage Statistics - **Total API M...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~28-~28: Use correct spacing
Context: ...ction: ``` ### API Coverage Statistics - Total API Methods: 101 unique endpoint...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~44-~44: Use correct spacing
Context: ...gout, GraphQL) ### Interactive Features - Enhanced User Experience: Categorized ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~50-~50: Use correct spacing
Context: ... export functionality for all data types Donate via PayPal Sponsor on GitHub A comprehensive Python 3 API wrapper for...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~55-~55: Use correct spacing
Context: ...ess to health, fitness, and device data. ## 📖 About This library enables developer...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a problem here.
Context: ... programmatically access Garmin Connect data including: - Health Metrics: Heart rate, sleep, str...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~65-~65: Use correct spacing
Context: ...s, progress tracking, date range queries Compatible with all Garmin Connect accou...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~67-~67: There might be a mistake here.
Context: ...counts. See https://connect.garmin.com/ ## 📦 Installation Install from PyPI: ```...

(QB_NEW_EN_OTHER)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~71-~71: Use correct spacing
Context: ... ## 📦 Installation Install from PyPI: bash pip3 install garminconnect ## 🔐 Authentication The library uses the ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~77-~77: There might be a mistake here.
Context: ... garminconnect ``` ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~79-~79: Use correct spacing
Context: ...Garth. Key Features: - Login credentials vali...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~81-~81: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~82-~82: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authe...

(QB_NEW_EN)


[grammar] ~83-~83: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~84-~84: Use correct spacing
Context: ...Same authentication flow as official app Advanced Configuration: ```python # Op...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~86-~86: There might be a problem here.
Context: ... official app Advanced Configuration: python # Optional: Custom OAuth consumer (before login) import garth garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'} Token Storage: Tokens are automatically sav...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~93-93: There might be a mistake here.
Context: ...: 'your_secret'} ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~94-~94: Use articles correctly
Context: ...orage:** Tokens are automatically saved to ~/.garminconnect directory for persis...

(QB_NEW_EN_OTHER_ERROR_IDS_11)


[grammar] ~94-~94: Use correct spacing
Context: ...directory for persistent authentication. ## 🧪 Testing Run the test suite to verify...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~96-~96: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~98-~98: Use correct spacing
Context: ... the test suite to verify functionality: Prerequisites: ```bash # Set token dir...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~100-~100: Use correct spacing
Context: ... verify functionality: Prerequisites: bash # Set token directory (uses example.py credentials) export GARMINTOKENS=~/.garminconnect # Install pytest (if needed) sudo apt install python3-pytest Run Tests: ```bash make install-test m...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~109-~109: Use correct spacing
Context: ...install python3-pytest **Run Tests:**bash make install-test make test ``` Note: Test files use credential tokens...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~115-~115: Use correct spacing
Context: ...first to generate authentication tokens. ## 🛠️ Development Set up a development en...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~117-~117: There might be a mistake here.
Context: ...thentication tokens. ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~119-~119: Use correct spacing
Context: ...evelopment environment for contributing: Environment Setup: ```bash make .venv ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~121-~121: Use correct spacing
Context: ... for contributing: Environment Setup: bash make .venv source .venv/bin/activate pip3 install pdm ruff pdm init Development Tools: ```bash # Install c...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~130-~130: Use correct spacing
Context: ... ruff pdm init **Development Tools:**bash # Install code quality tools sudo apt install pre-commit isort black mypy pip3 install pre-commit **Code Quality Checks:**bash make form...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~137-~137: Use correct spacing
Context: ...l pre-commit **Code Quality Checks:**bash make format # Format code make lint # Lint code make codespell # Check spelling ``` Run these commands before submitting PRs...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~144-~144: Use correct spacing
Context: ...ng PRs to ensure code quality standards. ## 📦 Publishing For package maintainers: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~146-~146: There might be a mistake here.
Context: ...ode quality standards. ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~148-~148: There might be a problem here.
Context: ... 📦 Publishing For package maintainers: Setup PyPI credentials: ```bash sudo apt in...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~150-~150: Use correct spacing
Context: ... maintainers: Setup PyPI credentials: bash sudo apt install twine vi ~/.pypirc ini [pypi] username = __token__ password = <PyPI_API_TOKEN> Publish new version: ```bash make publ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~161-~161: Use correct spacing
Context: ...I_API_TOKEN> **Publish new version:**bash make publish ``` ## 🤝 Contributing We welcome contribution...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~166-~166: There might be a mistake here.
Context: ...ash make publish ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~168-~168: Use correct spacing
Context: ... contributions! Here's how you can help: - Report Issues: Bug reports and feature...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~170-~170: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~171-~171: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~172-~172: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~173-~173: Use correct spacing
Context: ...prove examples, add use cases, fix typos Before Contributing: 1. Run developmen...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~175-~175: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Run development setup (make .venv) 2. ...

(QB_NEW_EN)


[grammar] ~176-~176: There might be a mistake here.
Context: ... 1. Run development setup (make .venv) 2. Execute code quality checks (`make forma...

(QB_NEW_EN_OTHER)


[grammar] ~179-~179: There might be a mistake here.
Context: ... Follow existing code style and patterns ## 💻 Usage Examples ### Interactive Demo ...

(QB_NEW_EN_OTHER)


[grammar] ~181-~181: There might be a mistake here.
Context: ...style and patterns ## 💻 Usage Examples ### Interactive Demo Run the comprehensive A...

(QB_NEW_EN)


[grammar] ~183-~183: There might be a mistake here.
Context: ... 💻 Usage Examples ### Interactive Demo Run the comprehensive API demonstration:...

(QB_NEW_EN)


[grammar] ~184-~184: Use correct spacing
Context: ...Run the comprehensive API demonstration: bash pip3 install -r requirements-dev.txt ./example.py ### Jupyter Notebook Explore the API interac...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~190-~190: There might be a mistake here.
Context: ...t ./example.py ``` ### Jupyter Notebook Explore the API interactively with our [...

(QB_NEW_EN)


[grammar] ~191-~191: Use correct spacing
Context: ...minconnect/blob/master/reference.ipynb). ### Python Code Examples ```python from garm...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~193-~193: Use correct spacing
Context: ...erence.ipynb). ### Python Code Examples python from garminconnect import Garmin # Initialize and login client = Garmin('your_email', 'your_password') client.login() # Get today's stats stats = client.get_stats('2023-08-31') print(f"Steps: {stats['totalSteps']}") # Get heart rate data hr_data = client.get_heart_rates('2023-08-31') print(f"Resting HR: {hr_data['restingHeartRate']}") ### Additional Resources - Source Code: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~210-~210: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~211-~211: Use an accepted time format
Context: ...Resources - Source Code: example.py - API Documentation: Comprehensive metho...

(QB_NEW_EN_OTHER_ERROR_IDS_4)


[grammar] ~212-~212: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~213-~213: Use correct spacing
Context: ...rld usage examples in tests/ directory ## 🙏 Acknowledgments Special thanks to al...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~215-~215: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~217-~217: Use correct spacing
Context: ...rs who have helped improve this project: - Community Contributors: Bug reports, f...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~219-~219: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~220-~220: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~221-~221: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~222-~222: Use correct spacing
Context: ...rs**: Improving examples and user guides This project thrives thanks to community...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~224-~224: Use correct spacing
Context: ...s to community involvement and feedback. ## 💖 Support This Project If you find thi...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~226-~226: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~228-~228: Use correct spacing
Context: ...s continued development and maintenance: ### 🌟 Ways to Support - **⭐ Star this repo...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~230-~230: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[typographical] ~232-~232: To join two clauses or set off examples, consider using an em dash.
Context: ...s to Support - ⭐ Star this repository - Help others discover the project - **💰 Fina...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~232-~232: There might be a mistake here.
Context: ...ory** - Help others discover the project - 💰 Financial Support - Contribute to d...

(QB_NEW_EN_OTHER)


[typographical] ~233-~233: To join two clauses or set off examples, consider using an em dash.
Context: ...ver the project - 💰 Financial Support - Contribute to development and hosting costs - **🐛...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~233-~233: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[typographical] ~234-~234: To join two clauses or set off examples, consider using an em dash.
Context: ...t and hosting costs - 🐛 Report Issues - Help improve stability and compatibility -...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~234-~234: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[typographical] ~235-~235: To join two clauses or set off examples, consider using an em dash.
Context: ...d compatibility - 📖 Spread the Word - Share with other developers ### 💳 Financial...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~235-~235: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~237-~237: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~243-~243: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~244-~244: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~245-~245: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~246-~246: Use correct spacing
Context: ...ion for hundreds of hours of development Every contribution, no matter the size, ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~248-~248: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

28-28: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


82-82: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


83-83: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


87-87: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


101-101: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


110-110: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


122-122: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


131-131: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


138-138: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


151-151: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


154-154: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


162-162: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


176-176: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


177-177: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


183-183: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


185-185: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


190-190: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


193-193: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


194-194: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


210-210: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


211-211: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


243-243: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

🪛 Gitleaks (8.27.2)
garminconnect/__init__.py

353-353: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (11)
.gitignore (2)

2-2: Good: local data kept out of VCS.

Ignoring your_data/ is sensible for examples and local runs.


25-27: Broad ignores (lib/, lib64/). Verify they won’t hide source.

If you ever add package subdirs named lib/ or lib64/ under the repo, they’ll be ignored. Confirm this is intentional.

LICENSE (1)

3-3: LGTM: year updated to 2025.

No license text changes; safe.

.editorconfig (1)

13-13: Note: max_line_length is advisory.

Many editors don’t enforce this EditorConfig key; rely on Black/Ruff for hard limits (already configured).

pyproject.toml (5)

30-32: *pytest addopts ignores .yaml; confirm test data discovery.

If future tests use YAML in tests/, this will skip them.


9-10: Dependency pin range sanity check.

garth>=0.5.13,<0.6.0 aligns with semver; verify new API calls rely on ≥0.5.13.


73-75: Ruff target-version matches requires-python (py310).

Good alignment.


33-35: ignore_missing_imports = true hides type issues.

Acceptable short-term; plan to tighten once typing in garminconnect/ stabilizes.


104-116: Coverage exclude regex for main is broken.

Use a literal string so Coverage recognizes it.

 exclude_lines = [
 ...
-    "if __name__ == .__main__.:",
+    "if __name__ == \"__main__\":",
 ...
 ]

Likely an incorrect or invalid review comment.

test_data/sample_activity.gpx (2)

6-6: Timestamp uses today (2025-08-31).

Good for freshness; ensure tests won’t be time-sensitive.


12-20: Minor: elevation -1.0 m in Amsterdam is plausible.

No action; just noting realism of sample.

Comment on lines +21 to +23
[*.{js,json}]
indent_size = 2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add explicit TOML stanza for consistency.

Make indentation explicit for *.toml to match project style.

 [*.{js,json}]
 indent_size = 2

+[*.toml]
+indent_size = 4
🤖 Prompt for AI Agents
In .editorconfig around lines 21 to 23, add an explicit TOML stanza so TOML
files inherit the same indentation rules as the rest of the project; create a
new block [*.toml] (or include toml in the existing glob) and set indent_size =
2 (and optionally indent_style = space) to make TOML indentation explicit and
consistent.

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Test Activity Creator" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Declare gpxtpx namespace at root.

This avoids repeating xmlns on every TrackPointExtension and improves compatibility.

-<gpx version="1.1" creator="Test Activity Creator" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<gpx version="1.1" creator="python-garminconnect-tests"
+     xmlns="http://www.topografix.com/GPX/1/1"
+     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+     xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
+     xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<gpx version="1.1" creator="Test Activity Creator" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<gpx version="1.1" creator="python-garminconnect-tests"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
🤖 Prompt for AI Agents
In test_data/sample_activity.gpx around line 2, the GPX root element is missing
the gpxtpx namespace; add a gpxtpx namespace declaration
(xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1") to the
<gpx> root so TrackPointExtension elements can use the gpxtpx: prefix and you
avoid repeating xmlns on every TrackPointExtension for compatibility.

<ele>-1.0</ele>
<time>2025-08-31T10:00:00Z</time>
<extensions>
<gpxtpx:TrackPointExtension xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove repeated xmlns declarations from each TrackPointExtension.

After moving the namespace to root, drop local xmlns:gpxtpx to reduce verbosity.

Example change (apply to all occurrences):

-          <gpxtpx:TrackPointExtension xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1">
+          <gpxtpx:TrackPointExtension>

Also applies to: 25-25, 34-34, 43-43, 52-52, 61-61, 70-70, 79-79, 88-88, 97-97, 106-106, 115-115, 124-124, 133-133, 142-142

🤖 Prompt for AI Agents
In test_data/sample_activity.gpx around lines
16,25,34,43,52,61,70,79,88,97,106,115,124,133,142 the TrackPointExtension
elements still declare xmlns:gpxtpx locally; since the namespace was moved to
the root, remove the repeated
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" attribute
from each <gpxtpx:TrackPointExtension> occurrence (and any identical local xmlns
attribute) so only the root-level namespace remains, ensuring no other
attributes or content are changed.

Comment on lines +1 to +254
{
"workoutId": 1055637,
"ownerId": 10788552,
"workoutName": "Simple session",
"updatedDate": "2018-07-05T17:34:04.0",
"createdDate": "2018-05-08T09:14:50.0",
"sportType": {
"sportTypeId": 1,
"sportTypeKey": "running",
"displayOrder": 1
},
"author": {},
"estimatedDurationInSecs": 1200,
"workoutSegments": [
{
"segmentOrder": 1,
"sportType": {
"sportTypeId": 1,
"sportTypeKey": "running",
"displayOrder": 1
},
"workoutSteps": [
{
"type": "ExecutableStepDTO",
"stepId": 633927266,
"stepOrder": 1,
"stepType": {
"stepTypeId": 1,
"stepTypeKey": "warmup",
"displayOrder": 1
},
"endCondition": {
"conditionTypeId": 2,
"conditionTypeKey": "time",
"displayOrder": 2,
"displayable": true
},
"endConditionValue": 180.0,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target",
"displayOrder": 1
},
"strokeType": {
"strokeTypeId": 0,
"displayOrder": 0
},
"equipmentType": {
"equipmentTypeId": 0,
"displayOrder": 0
}
},
{
"type": "RepeatGroupDTO",
"stepId": 633927267,
"stepOrder": 2,
"stepType": {
"stepTypeId": 6,
"stepTypeKey": "repeat",
"displayOrder": 6
},
"childStepId": 1,
"numberOfIterations": 6,
"workoutSteps": [
{
"type": "ExecutableStepDTO",
"stepId": 705526052,
"stepOrder": 3,
"stepType": {
"stepTypeId": 3,
"stepTypeKey": "interval",
"displayOrder": 3
},
"childStepId": 1,
"endCondition": {
"conditionTypeId": 2,
"conditionTypeKey": "time",
"displayOrder": 2,
"displayable": true
},
"endConditionValue": 60.0,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target",
"displayOrder": 1
},
"strokeType": {
"strokeTypeId": 0,
"displayOrder": 0
},
"equipmentType": {
"equipmentTypeId": 0,
"displayOrder": 0
}
},
{
"type": "ExecutableStepDTO",
"stepId": 705526053,
"stepOrder": 4,
"stepType": {
"stepTypeId": 4,
"stepTypeKey": "recovery",
"displayOrder": 4
},
"childStepId": 1,
"endCondition": {
"conditionTypeId": 2,
"conditionTypeKey": "time",
"displayOrder": 2,
"displayable": true
},
"endConditionValue": 60.0,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target",
"displayOrder": 1
},
"strokeType": {
"strokeTypeId": 0,
"displayOrder": 0
},
"equipmentType": {
"equipmentTypeId": 0,
"displayOrder": 0
}
}
],
"endConditionValue": 6.0,
"endCondition": {
"conditionTypeId": 7,
"conditionTypeKey": "iterations",
"displayOrder": 7,
"displayable": false
},
"smartRepeat": false
},
{
"type": "RepeatGroupDTO",
"stepId": 633927270,
"stepOrder": 5,
"stepType": {
"stepTypeId": 6,
"stepTypeKey": "repeat",
"displayOrder": 6
},
"childStepId": 2,
"numberOfIterations": 3,
"workoutSteps": [
{
"type": "ExecutableStepDTO",
"stepId": 633927271,
"stepOrder": 6,
"stepType": {
"stepTypeId": 3,
"stepTypeKey": "interval",
"displayOrder": 3
},
"childStepId": 2,
"endCondition": {
"conditionTypeId": 2,
"conditionTypeKey": "time",
"displayOrder": 2,
"displayable": true
},
"endConditionValue": 30.0,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target",
"displayOrder": 1
},
"strokeType": {
"strokeTypeId": 0,
"displayOrder": 0
},
"equipmentType": {
"equipmentTypeId": 0,
"displayOrder": 0
}
},
{
"type": "ExecutableStepDTO",
"stepId": 633927272,
"stepOrder": 7,
"stepType": {
"stepTypeId": 4,
"stepTypeKey": "recovery",
"displayOrder": 4
},
"childStepId": 2,
"endCondition": {
"conditionTypeId": 2,
"conditionTypeKey": "time",
"displayOrder": 2,
"displayable": true
},
"endConditionValue": 30.0,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target",
"displayOrder": 1
},
"strokeType": {
"strokeTypeId": 0,
"displayOrder": 0
},
"equipmentType": {
"equipmentTypeId": 0,
"displayOrder": 0
}
}
],
"endConditionValue": 3.0,
"endCondition": {
"conditionTypeId": 7,
"conditionTypeKey": "iterations",
"displayOrder": 7,
"displayable": false
},
"smartRepeat": false
},
{
"type": "ExecutableStepDTO",
"stepId": 633927273,
"stepOrder": 8,
"stepType": {
"stepTypeId": 2,
"stepTypeKey": "cooldown",
"displayOrder": 2
},
"endCondition": {
"conditionTypeId": 2,
"conditionTypeKey": "time",
"displayOrder": 2,
"displayable": true
},
"endConditionValue": 120.0,
"targetType": {
"workoutTargetTypeId": 1,
"workoutTargetTypeKey": "no.target",
"displayOrder": 1
},
"strokeType": {
"strokeTypeId": 0,
"displayOrder": 0
},
"equipmentType": {
"equipmentTypeId": 0,
"displayOrder": 0
}
}
]
}
]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate payload against upload_workout expectations.

Your new upload_workout currently accepts a str but posts it via json=, which expects a mapping; posting a raw string will serialize as a JSON string, likely causing a 4xx. Either ship this file as dict in tests/examples or change the API method to parse strings.


🧹 Nitpick (assertive)

Sanitize or annotate IDs in sample payload.

workoutId and ownerId look real. Prefer 0/placeholder values or add a comment in README noting these are anonymized to avoid accidental identification or server lookups if reused verbatim.

🤖 Prompt for AI Agents
In test_data/sample_workout.json around lines 1 to 254 the top-level workoutId
and ownerId contain what look like real identifiers; replace them with
anonymized placeholder values (e.g., 0 or "REDACTED") in the JSON and/or add a
short note in the repo README explaining that sample payloads have been
sanitized and must not be used verbatim for server requests; update the sample
file and README accordingly so tests/examples use non-identifying IDs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
garminconnect/__init__.py (5)

727-739: Validate cdate in get_body_battery_events.

Apply:

-        url = f"{self.garmin_connect_body_battery_events_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_body_battery_events_url}/{cdate}"

797-804: Validate cdate in get_max_metrics.

Apply:

-        url = f"{self.garmin_connect_metrics_url}/{cdate}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_metrics_url}/{cdate}/{cdate}"

805-886: Fix typing (pipeline F821) and align with builtin generics.

Optional/Dict not imported; prefer builtin generics.

Apply:

-    def get_lactate_threshold(self, *,latest: bool=True, start_date: Optional[str|date]=None, end_date: Optional[str|date]=None, aggregation: str ="daily") -> Dict:
+    def get_lactate_threshold(
+        self,
+        *,
+        latest: bool = True,
+        start_date: str | date | None = None,
+        end_date: str | date | None = None,
+        aggregation: str = "daily",
+    ) -> dict[str, Any]:

1016-1023: Fix typing (pipeline F821) for get_earned_badges.

Apply:

-    def get_earned_badges(self) -> list[Dict[str, Any]]:
+    def get_earned_badges(self) -> list[dict[str, Any]]:

906-946: Timestamp parsing is strict to microseconds; consider accepting second-precision.

If you want to accept “YYYY-MM-DDTHH:MM:SS” too, try fromisoformat with fallback or strptime variants.

♻️ Duplicate comments (7)
garminconnect/__init__.py (7)

957-1007: Validate cdate in hydration/respiration/spo2/intensity/stress/events getters.

Prevents 4xx on bad dates and aligns with other getters.

Apply:

-        url = f"{self.garmin_connect_daily_hydration_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_daily_hydration_url}/{cdate}"
@@
-        url = f"{self.garmin_connect_daily_respiration_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_daily_respiration_url}/{cdate}"
@@
-        url = f"{self.garmin_connect_daily_spo2_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_daily_spo2_url}/{cdate}"
@@
-        url = f"{self.garmin_connect_daily_intensity_minutes}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_daily_intensity_minutes}/{cdate}"
@@
-        url = f"{self.garmin_all_day_stress_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_all_day_stress_url}/{cdate}"
@@
-        url = f"{self.garmin_daily_events_url}?calendarDate={cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_daily_events_url}?calendarDate={cdate}"

1120-1165: Add date validation (and optional uniform error handling) across these getters.

Validate cdate; optionally mirror get_user_summary’s empty/privacy checks.

Apply:

-        url = f"{self.garmin_connect_daily_sleep_url}/{self.display_name}"
-        params = {"date": str(cdate), "nonSleepBufferMinutes": 60}
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_daily_sleep_url}/{self.display_name}"
+        params = {"date": cdate, "nonSleepBufferMinutes": 60}
@@
-        url = f"{self.garmin_connect_daily_stress_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_daily_stress_url}/{cdate}"
@@
-        url = f"{self.garmin_connect_rhr_url}/{self.display_name}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_rhr_url}/{self.display_name}"
@@
-        url = f"{self.garmin_connect_hrv_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_hrv_url}/{cdate}"
@@
-        url = f"{self.garmin_connect_training_readiness_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_training_readiness_url}/{cdate}"

309-379: Unify login return type and document it.

Make the signature and docstring explicit that a 2-tuple of optional tokens is always returned.

Apply:

-    def login(self, /, tokenstore: str | None = None) -> tuple[Any, Any]:
-        """Log in using Garth."""
+    def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | None]:
+        """Log in using Garth.
+
+        Returns:
+            Tuple[str | None, str | None]: (access_token, refresh_token) when using credential flow;
+            (None, None) when loading from tokenstore.
+        """
@@
-                if self.username and '@' not in self.username:
+                if (not self.is_cn) and self.username and '@' not in self.username:
                     raise GarminConnectAuthenticationError("Email must contain '@' symbol")

1353-1359: Require integer limit in get_activities.

Use the positive-int validator added above; floats should be rejected.

Apply:

-        limit = _validate_positive_number(limit, "limit")
+        limit = _validate_positive_integer(limit, "limit")

1864-1871: Accept dict or JSON string in upload_workout.

json= expects a mapping/list; parsing strings prevents posting raw JSON text.

Apply:

-    def upload_workout(self, workout_json: str):
+    def upload_workout(self, workout_json: dict[str, Any] | list[Any] | str):
         """Upload workout using json data."""
@@
-        return self.garth.post("connectapi", url, json=workout_json, api=True)
+        if isinstance(workout_json, str):
+            import json as _json
+            try:
+                payload = _json.loads(workout_json)
+            except Exception as e:
+                raise ValueError(f"Invalid workout_json string: {e}") from e
+        else:
+            payload = workout_json
+        return self.garth.post("connectapi", url, json=payload, api=True)

1912-1914: Use warning for deprecation notice, not error.

Apply:

-        logger.error(
+        logger.warning(
             "Deprecated: Alternative is to delete the login tokens to logout."
         )

54-62: Add a positive-integer validator (and use it for count-like params).

Needed for params like limit to forbid floats/zero.

Apply:

 def _validate_non_negative_integer(value: int, param_name: str = "value") -> int:
@@
     return value
+
+def _validate_positive_integer(value: int, param_name: str = "value") -> int:
+    """Validate that a value is a positive integer."""
+    if not isinstance(value, int):
+        raise ValueError(f"{param_name} must be an integer")
+    if value <= 0:
+        raise ValueError(f"{param_name} must be a positive integer, got: {value}")
+    return value
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b61aad and a1aa4f4.

📒 Files selected for processing (1)
  • garminconnect/__init__.py (33 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (459-511)
  • timestamp (404-409)
🪛 Gitleaks (8.27.2)
garminconnect/__init__.py

357-357: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 GitHub Actions: CI
garminconnect/__init__.py

[error] 805-805: F821 Undefined name 'Optional'.


[error] 805-805: F821 Undefined name 'Dict'.


[error] 1016-1016: F821 Undefined name 'Dict'.

🔇 Additional comments (8)
garminconnect/__init__.py (8)

78-87: LGTM: constructor input validation.


418-437: LGTM: consistent date validation and response checks in get_user_summary.


438-455: LGTM: steps endpoint validation and safe empty return.


472-490: LGTM: start/end date validation and ordering check.


491-519: LGTM: clear docstring and validation for heart rates.


1275-1300: LGTM: device endpoints.


1459-1506: LGTM: robust file validation and safe open in upload_activity.


357-357: False-positive UUIDs flagged; no secrets present. The RG scan only matched JSON “uuid” fields (32-hex char identifiers), not API keys or tokens—no action needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
garminconnect/__init__.py (8)

544-557: Validate start/end dates in get_body_composition().

 def get_body_composition(
     self, startdate: str, enddate=None
 ) -> dict[str, Any]:
@@
-    if enddate is None:
-        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate or startdate, "enddate")
@@
-    params = {"startDate": str(startdate), "endDate": str(enddate)}
+    params = {"startDate": startdate, "endDate": enddate}

725-739: Validate dates in get_body_battery().

-    if enddate is None:
-        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate or startdate, "enddate")
@@
-    params = {"startDate": str(startdate), "EndDate": str(enddate)}
+    params = {"startDate": startdate, "endDate": enddate}

Note: fix endDate capitalization if mismatched.


784-799: Validate date params in get_blood_pressure().

-    if enddate is None:
-        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate or startdate, "enddate")
@@
-    url = f"{self.garmin_connect_blood_pressure_endpoint}/{startdate}/{enddate}"
+    url = f"{self.garmin_connect_blood_pressure_endpoint}/{startdate}/{enddate}"

867-881: Prevent KeyError on misspelled/optional keys in get_lactate_threshold().

-            for entry in speed_and_heart_rate:
-                if entry['speed'] is not None:
+            for entry in speed_and_heart_rate:
+                if entry.get('speed') is not None:
                     speed_and_heart_rate_dict["userProfilePK"] = entry["userProfilePK"]
                     speed_and_heart_rate_dict["version"] = entry["version"]
                     speed_and_heart_rate_dict["calendarDate"] = entry["calendarDate"]
                     speed_and_heart_rate_dict["sequence"] = entry["sequence"]
                     speed_and_heart_rate_dict["speed"] = entry["speed"]
 
                 # This is not a typo. The Garmin dictionary has a typo as of 2025-07-08, refering to it as "hearRate"
-                elif entry['hearRate'] is not None:
-                    speed_and_heart_rate_dict["heartRate"] = entry["hearRate"] # Fix Garmin's typo
+                elif entry.get('hearRate') is not None:
+                    speed_and_heart_rate_dict["heartRate"] = entry["hearRate"]  # Fix Garmin's typo
 
                 # Doesn't exist for me but adding it just in case.  We'll check for each entry
-                if entry['heartRateCycling'] is not None:
-                    speed_and_heart_rate_dict["heartRateCycling"] = entry["heartRateCycling"]
+                if entry.get('heartRateCycling') is not None:
+                    speed_and_heart_rate_dict["heartRateCycling"] = entry["heartRateCycling"]

1277-1284: Validate cdate in get_fitnessage_data().

-    url = f"{self.garmin_connect_fitnessage}/{cdate}"
+    cdate = _validate_date_format(cdate, "cdate")
+    url = f"{self.garmin_connect_fitnessage}/{cdate}"

1336-1350: Validate start/end dates in get_device_solar_data().

-    if enddate is None:
+    startdate = _validate_date_format(startdate, "startdate")
+    if enddate is None:
         enddate = startdate
         single_day = True
     else:
+        enddate = _validate_date_format(enddate, "enddate")
         single_day = False
@@
-    url = f"{self.garmin_connect_solar_url}/{device_id}/{startdate}/{enddate}"
+    url = f"{self.garmin_connect_solar_url}/{device_id}/{startdate}/{enddate}"

1356-1363: Avoid KeyError if device settings lack alarms.

-            device_alarms = device_settings["alarms"]
-            if device_alarms is not None:
+            device_alarms = device_settings.get("alarms")
+            if device_alarms:
                 alarms += device_alarms

910-979: Add leading slash to set_hydration_url constant
In garminconnect/init.py at the definition of self.garmin_connect_set_hydration_url, prefix the path with “/” to match other endpoints and prevent broken URL joins:

-        self.garmin_connect_set_hydration_url = (
-            "usersummary-service/usersummary/hydration/log"
-        )
+        self.garmin_connect_set_hydration_url = (
+            "/usersummary-service/usersummary/hydration/log"
+        )

• Optional: in add_hydration_data, guard against regression by normalizing the URL at use:

-        url = self.garmin_connect_set_hydration_url
+        url = f"/{self.garmin_connect_set_hydration_url.lstrip('/')}"
♻️ Duplicate comments (7)
.github/workflows/ci.yml (5)

3-3: Quote the workflow trigger key to satisfy strict YAML linters.

-on:
+"on":

5-7: Normalize array bracket spacing.

-    branches: [ main, master ]
+    branches: [main, master]
@@
-    branches: [ main, master ]
+    branches: [main, master]

16-23: Fix steps indentation (YAML parse/lint error).

-    steps:
-    - uses: actions/checkout@v4
+    steps:
+      - uses: actions/checkout@v4
@@
-    steps:
-    - uses: actions/checkout@v4
+    steps:
+      - uses: actions/checkout@v4

Also applies to: 56-76


20-23: Bump setup-python to v5 (v4 deprecated on GH-hosted runners).

-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
@@
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5

Also applies to: 61-64


47-53: Coverage step reruns tests without required env and no artifact upload.

     - name: Upload coverage reports
       if: matrix.python-version == '3.11'
-      run: |
-        pip install coverage[toml]
-        coverage run -m pytest
-        coverage xml
+      env:
+        GARMINTOKENS: ${{ secrets.GARMINTOKENS }}
+      run: |
+        pip install coverage[toml]
+        coverage run -m pytest -v --tb=short
+        coverage xml
       continue-on-error: true
+
+    - name: Upload coverage artifact
+      if: matrix.python-version == '3.11'
+      uses: actions/upload-artifact@v4
+      with:
+        name: coverage-xml
+        path: coverage.xml
garminconnect/__init__.py (2)

1269-1276: Validate cdate in get_training_status().

-    url = f"{self.garmin_connect_training_status_url}/{cdate}"
+    cdate = _validate_date_format(cdate, "cdate")
+    url = f"{self.garmin_connect_training_status_url}/{cdate}"

295-308: Replace brittle string-matching with status-based HTTP error mapping.

 def connectapi(self, path, **kwargs):
-    """Wrapper for garth connectapi with error handling."""
-    try:
-        return self.garth.connectapi(path, **kwargs)
-    except Exception as e:
-        logger.error(f"API call failed for path '{path}': {e}")
-        # Re-raise with more context but preserve original exception type
-        if "auth" in str(e).lower() or "401" in str(e):
-            raise GarminConnectAuthenticationError(f"Authentication failed: {e}") from e
-        elif "429" in str(e) or "rate" in str(e).lower():
-            raise GarminConnectTooManyRequestsError(f"Rate limit exceeded: {e}") from e
-        else:
-            raise GarminConnectConnectionError(f"Connection error: {e}") from e
+    """Wrapper for garth connectapi with error handling."""
+    try:
+        return self.garth.connectapi(path, **kwargs)
+    except Exception as e:
+        status = getattr(getattr(e, "response", None), "status_code", None)
+        msg = f"API call failed for path '{path}': {e}"
+        logger.error("%s%s", msg, f" (status={status})" if status else "")
+        if status == 401:
+            raise GarminConnectAuthenticationError(msg) from e
+        if status == 429:
+            raise GarminConnectTooManyRequestsError(msg) from e
+        raise GarminConnectConnectionError(msg) from e

Apply similar handling to download() if appropriate.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a1aa4f4 and 2567806.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml (1 hunks)
  • garminconnect/__init__.py (30 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (459-511)
  • timestamp (404-409)
🪛 actionlint (1.7.7)
.github/workflows/ci.yml

20-20: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


61-61: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 YAMLlint (1.37.1)
.github/workflows/ci.yml

[warning] 3-3: truthy value should be one of [false, true]

(truthy)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 7-7: too many spaces inside brackets

(brackets)


[error] 7-7: too many spaces inside brackets

(brackets)


[error] 17-17: wrong indentation: expected 6 but found 4

(indentation)


[error] 58-58: wrong indentation: expected 6 but found 4

(indentation)

🪛 Gitleaks (8.27.2)
garminconnect/__init__.py

371-371: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 GitHub Actions: CI
garminconnect/__init__.py

[error] 3-3: Ruff: I001 - Import block is unsorted or un-formatted.

🔇 Additional comments (4)
garminconnect/__init__.py (4)

432-451: Nice: consistent input validation and clearer error handling across getters.

Also applies to: 452-469, 470-485, 505-533, 741-753, 980-987, 989-997, 998-1006, 1007-1015, 1016-1024, 1025-1036, 1149-1158, 1159-1167, 1168-1181, 1182-1190, 1191-1199, 1309-1316, 1317-1324, 1325-1334


1954-1956: Correct log level for deprecation notice.

Switching to warning is appropriate.


3-13: Import order corrected. Ruff I001 no longer reported.


1-1973: No hardcoded secrets found
Regex scan returned no matches, confirming there are no hardcoded API keys, tokens, or secrets in this file.

Comment on lines 1 to 9
name: CI

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Optional: cancel redundant workflow runs to save CI minutes.

 name: CI
 
 "on":
+  workflow_dispatch:
   push:
     branches: [main, master]
   pull_request:
     branches: [main, master]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: CI
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
name: CI
"on":
workflow_dispatch:
push:
branches: [main, master]
pull_request:
branches: [main, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 3-3: truthy value should be one of [false, true]

(truthy)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 5-5: too many spaces inside brackets

(brackets)


[error] 7-7: too many spaces inside brackets

(brackets)


[error] 7-7: too many spaces inside brackets

(brackets)

🤖 Prompt for AI Agents
.github/workflows/ci.yml lines 1-9: CI workflow currently triggers on both push
and pull_request for the same branches causing redundant runs; add a concurrency
configuration to cancel redundant in-progress workflows (set a deterministic
group name like the workflow plus ref or PR identifier and enable
cancel-in-progress true) so only the latest run for the same branch/PR
continues.

Comment on lines +9 to +15
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Optional: add pip cache to speed installs.

     strategy:
       matrix:
         python-version: ["3.10", "3.11", "3.12", "3.13"]
 
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
+          cache: 'pip'

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .github/workflows/ci.yml around lines 9–15 the test job runs multiple Python
versions but doesn't cache pip downloads; add a cache step to speed installs by
inserting an actions/cache step before pip install that caches the pip
wheel/cache directory (e.g. ~/.cache/pip), use a key that includes the runner
and matrix python-version and a fingerprint of dependency files
(requirements.txt/pyproject.lock) and include restore-keys for partial matches
so cached packages are reused across runs and Python versions.

Comment on lines 317 to 324
def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | None]:
"""
Log in using Garth.

Returns:
Tuple[str | None, str | None]: (access_token, refresh_token) when using credential flow;
(None, None) when loading from tokenstore.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

MFA flow: document/display_name availability and return shape.

When return_on_mfa=True, you return tokens but skip profile/unit settings. Confirm callers won’t access display_name/unit_system before resume_login. If needed, explicitly document this in login() docstring and consider returning a structured result (e.g., tokens plus client_state) to reduce footguns.

Also applies to: 334-353, 363-393

🤖 Prompt for AI Agents
In garminconnect/__init__.py around lines 317 to 324 (and similarly at 334-353
and 363-393), the MFA branch that returns early with return_on_mfa=True can
return tokens while skipping profile/unit initialization, meaning display_name
and unit_system may be absent; update the login() docstring to explicitly state
that when return_on_mfa=True the function may return only tokens and client
state and that profile/display_name/unit_system will not be populated until
resume_login, and to prevent footguns either (a) change the return shape to a
structured result (e.g., a dataclass or dict with keys like access_token,
refresh_token, client_state, profile_loaded_flag) or (b) add client_state to the
existing return and clearly document which fields are present in each flow; pick
one approach, update all affected branches/comments and adjust callers or tests
accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
garminconnect/__init__.py (6)

1278-1285: Validate cdate in get_fitnessage_data().

Keep input handling consistent.

Apply:

     def get_fitnessage_data(self, cdate: str) -> dict[str, Any]:
         """Return Fitness Age data for current user."""
-
-        url = f"{self.garmin_connect_fitnessage}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_fitnessage}/{cdate}"
         logger.debug("Requesting Fitness Age data")
 
         return self.connectapi(url)

545-557: body composition: validate dates and normalize.

Ensure start/end are real dates before request.

Apply:

     def get_body_composition(
         self, startdate: str, enddate=None
     ) -> dict[str, Any]:
@@
-        if enddate is None:
-            enddate = startdate
+        startdate = _validate_date_format(startdate, "startdate")
+        enddate = _validate_date_format(enddate or startdate, "enddate")
         url = f"{self.garmin_connect_weight_url}/weight/dateRange"
-        params = {"startDate": str(startdate), "endDate": str(enddate)}
+        params = {"startDate": startdate, "endDate": enddate}
         logger.debug("Requesting body composition")
 
         return self.connectapi(url, params=params)

786-800: blood pressure: validate date range inputs.

Prevents 4xx from bad dates.

Apply:

     def get_blood_pressure(
         self, startdate: str, enddate=None
     ) -> dict[str, Any]:
@@
-        if enddate is None:
-            enddate = startdate
-        url = f"{self.garmin_connect_blood_pressure_endpoint}/{startdate}/{enddate}"
+        startdate = _validate_date_format(startdate, "startdate")
+        enddate = _validate_date_format(enddate or startdate, "enddate")
+        url = f"{self.garmin_connect_blood_pressure_endpoint}/{startdate}/{enddate}"
         params = {"includeAll": True}
         logger.debug("Requesting blood pressure data")
 
         return self.connectapi(url, params=params)

1875-1878: request_reload(): validate cdate.

Align with other date-based endpoints.

Apply:

     def request_reload(self, cdate: str):
@@
-        url = f"{self.garmin_request_reload_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_request_reload_url}/{cdate}"
         logger.debug(f"Requesting reload of data for {cdate}.")
 
         return self.garth.post("connectapi", url, api=True)

889-899: Timestamp parsing: be lenient to common ISO8601 variants.

Strict “%Y-%m-%dT%H:%M:%S.%f” rejects “Z” or no fractional seconds. Consider accepting both.

I can prepare a helper that parses: with/without fractions, optional “Z”, and timezone offsets, then normalizes to your expected “.00” output. Want a patch?

Also applies to: 911-979


840-909: lactate threshold: normalize/validate dates when latest=False.

Currently accepts date | str; validate and normalize to YYYY-MM-DD.

Apply:

         if start_date is None:
             raise ValueError("You must either specify 'latest=True' or a start_date")
 
         if end_date is None:
             end_date = date.today().isoformat()
 
         _valid_aggregations = {"daily", "weekly", "monthly", "yearly"}
         if aggregation not in _valid_aggregations:
             raise ValueError(f"aggregation must be one of {_valid_aggregations}")
 
-        speed_url = f"{self.garmin_connect_biometric_stats_url}/lactateThresholdSpeed/range/{start_date}/{end_date}?sport=RUNNING&aggregation={aggregation}&aggregationStrategy=LATEST"
+        if isinstance(start_date, date):
+            start_s = start_date.isoformat()
+        else:
+            start_s = _validate_date_format(str(start_date), "start_date")
+        if isinstance(end_date, date):
+            end_s = end_date.isoformat()
+        else:
+            end_s = _validate_date_format(str(end_date), "end_date")
+
+        speed_url = f"{self.garmin_connect_biometric_stats_url}/lactateThresholdSpeed/range/{start_s}/{end_s}?sport=RUNNING&aggregation={aggregation}&aggregationStrategy=LATEST"
 
-        heart_rate_url = f"{self.garmin_connect_biometric_stats_url}/lactateThresholdHeartRate/range/{start_date}/{end_date}?sport=RUNNING&aggregation={aggregation}&aggregationStrategy=LATEST"
+        heart_rate_url = f"{self.garmin_connect_biometric_stats_url}/lactateThresholdHeartRate/range/{start_s}/{end_s}?sport=RUNNING&aggregation={aggregation}&aggregationStrategy=LATEST"
 
-        power_url = f"{self.garmin_connect_biometric_stats_url}/functionalThresholdPower/range/{start_date}/{end_date}?sport=RUNNING&aggregation={aggregation}&aggregationStrategy=LATEST"
+        power_url = f"{self.garmin_connect_biometric_stats_url}/functionalThresholdPower/range/{start_s}/{end_s}?sport=RUNNING&aggregation={aggregation}&aggregationStrategy=LATEST"
♻️ Duplicate comments (9)
.gitignore (1)

5-5: Duplicate .venv patterns; keep one canonical entry.

Retain only ".venv/" and drop the duplicate ".venv" later for consistency.

Apply:

 # Environments
 .env
 .envrc
-.venv
 env/
 venv/
 ENV/
 env.bak/
 venv.bak/

Also applies to: 118-121

pyproject.toml (1)

68-71: Optional: exclude non-runtime files from wheels/sdists.

Avoid shipping fixtures and CI files.

Apply:

 [tool.pdm]
 distribution = true
+
+[tool.pdm.build]
+excludes = [
+  "test_data/**",
+  ".github/**",
+]
.github/workflows/ci.yml (2)

23-27: Optional: enable pip cache in setup-python to speed installs.

Apply:

       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
+          cache: 'pip'

1-12: Optional: cancel redundant runs for same ref.

Saves CI minutes on rapid pushes.

Apply:

 name: CI
 
 "on":
   push:
     branches:
       - main
       - master
   pull_request:
     branches:
       - main
       - master
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
garminconnect/__init__.py (5)

22-25: Unused constant TIMESTAMP_FORMAT_STR.

Either use it consistently or remove to avoid drift.

Apply:

 DATE_FORMAT_STR = '%Y-%m-%d'
-TIMESTAMP_FORMAT_STR = '%Y-%m-%dT%H:%M:%S.%f'
 VALID_WEIGHT_UNITS = {"kg", "lbs"}

If you intend to standardize timestamps, introduce a constant that matches the two-decimal “.00” pattern you post today and refactor call sites.


296-308: Brittle error classification in connectapi(); key off HTTP status.

String matching on “auth/401/429” is unreliable.

Apply:

     def connectapi(self, path, **kwargs):
         """Wrapper for garth connectapi with error handling."""
         try:
             return self.garth.connectapi(path, **kwargs)
         except Exception as e:
-            logger.error(f"API call failed for path '{path}': {e}")
-            # Re-raise with more context but preserve original exception type
-            if "auth" in str(e).lower() or "401" in str(e):
-                raise GarminConnectAuthenticationError(f"Authentication failed: {e}") from e
-            elif "429" in str(e) or "rate" in str(e).lower():
-                raise GarminConnectTooManyRequestsError(f"Rate limit exceeded: {e}") from e
-            else:
-                raise GarminConnectConnectionError(f"Connection error: {e}") from e
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.error(
+                "API call failed for path '%s': %s%s",
+                path, e, f" (status={status})" if status else ""
+            )
+            if status == 401:
+                raise GarminConnectAuthenticationError(f"Authentication failed: {e}") from e
+            if status == 429:
+                raise GarminConnectTooManyRequestsError(f"Rate limit exceeded: {e}") from e
+            raise GarminConnectConnectionError(f"Connection error: {e}") from e

1270-1276: Validate cdate in get_training_status().

Add the same date validation used elsewhere.

Apply:

     def get_training_status(self, cdate: str) -> dict[str, Any]:
         """Return training status data for current user."""
-
-        url = f"{self.garmin_connect_training_status_url}/{cdate}"
+        cdate = _validate_date_format(cdate, "cdate")
+        url = f"{self.garmin_connect_training_status_url}/{cdate}"
         logger.debug("Requesting training status data")
 
         return self.connectapi(url)

310-316: Align download() error handling with connectapi().

Return typed errors based on status code.

Apply:

     def download(self, path, **kwargs):
         """Wrapper for garth download with error handling."""
         try:
             return self.garth.download(path, **kwargs)
         except Exception as e:
-            logger.error(f"Download failed for path '{path}': {e}")
-            raise GarminConnectConnectionError(f"Download error: {e}") from e
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.error(
+                "Download failed for path '%s': %s%s",
+                path, e, f" (status={status})" if status else ""
+            )
+            if status == 401:
+                raise GarminConnectAuthenticationError(f"Download error: {e}") from e
+            if status == 429:
+                raise GarminConnectTooManyRequestsError(f"Download error: {e}") from e
+            raise GarminConnectConnectionError(f"Download error: {e}") from e

1902-1916: upload_workout(): verify parsed payload type before posting.

Ensure payload is dict or list.

Apply:

     def upload_workout(self, workout_json: dict[str, Any] | list[Any] | str):
         """Upload workout using json data."""
@@
         else:
             payload = workout_json
+        if not isinstance(payload, (dict, list)):
+            raise ValueError("workout_json must be a JSON object or array")
         return self.garth.post("connectapi", url, json=payload, api=True)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2567806 and 0a36537.

📒 Files selected for processing (5)
  • .editorconfig (1 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • .gitignore (2 hunks)
  • garminconnect/__init__.py (32 hunks)
  • pyproject.toml (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (459-511)
  • timestamp (404-409)
🪛 Gitleaks (8.27.2)
garminconnect/__init__.py

372-372: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 GitHub Actions: CI
garminconnect/__init__.py

[error] 1534-1534: invalid-syntax: Expected an indented block after with statement.

🔇 Additional comments (2)
.editorconfig (1)

1-29: LGTM — consistent with Black/ruff configs.

Matches pyproject (line length 88, space indents). No further action.

garminconnect/__init__.py (1)

372-372: No secrets detected (false positive) The prompt_mfa=self.prompt_mfa finding is a low-entropy false positive and not a credential; ripgrep and gitleaks scans show no sensitive data in garminconnect/__init__.py.

Comment on lines 75 to 86
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ruff: per-file ignore uses S101 but Bandit rules aren’t enabled.

Either enable “S” or drop S101. Enabling “S” is preferred.

Apply:

 [tool.ruff.lint]
 select = [
     "E",   # pycodestyle errors
     "W",   # pycodestyle warnings
     "F",   # pyflakes
     "I",   # isort
     "B",   # flake8-bugbear
     "C4",  # flake8-comprehensions
     "UP",  # pyupgrade
     "ARG", # flake8-unused-arguments
     "SIM", # flake8-simplify
+    "S",   # flake8-bandit
 ]
 
 [tool.ruff.lint.per-file-ignores]
 "tests/*" = ["ARG", "S101"]

Also applies to: 93-95

🤖 Prompt for AI Agents
In pyproject.toml around lines 75 to 86 (and also lines 93-95), the Ruff
configuration references S101 via per-file ignores but the "S" rule set (Bandit
security rules) is not enabled; either add "S" to the select list to enable
Bandit rules or remove any references to S101 in per-file-ignores—preferably add
"S" to the select array so Bandit rules are active and S101 is valid.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
tests/conftest.py (1)

30-34: Handle case-insensitive Cookie header.

Requests/vcr may normalize header casing; sanitize both "Cookie" and "cookie" to avoid misses.

-    if "Cookie" in request.headers:
-        cookies = request.headers["Cookie"].split("; ")
+    cookie_key = "Cookie" if "Cookie" in request.headers else ("cookie" if "cookie" in request.headers else None)
+    if cookie_key:
+        cookies = request.headers[cookie_key].split("; ")
         sanitized_cookies = [sanitize_cookie(cookie) for cookie in cookies]
-        request.headers["Cookie"] = "; ".join(sanitized_cookies)
+        request.headers[cookie_key] = "; ".join(sanitized_cookies)
garminconnect/fit.py (5)

108-124: Incorrect field sizes for float32/float64.

FIT float32 is 4 bytes and float64 is 8 bytes; current sizes are wrong and will produce invalid records.

-    float32 = {
+    float32 = {
@@
-        "size": 2,
+        "size": 4,
     }
@@
-    float64 = {
+    float64 = {
@@
-        "size": 4,
+        "size": 8,
     }

157-176: Type hint mismatch in get_format.

Function uses basetype as a dict; fix annotation.

-    def get_format(basetype: int) -> str:
+    def get_format(basetype: dict[str, Any]) -> str:

379-391: CRC reads signed bytes and packs native-endian.

Use unsigned read and little-endian pack to match FIT spec.

-            crc = _calcCRC(crc, unpack("b", b)[0])
+            crc = _calcCRC(crc, unpack("B", b)[0])
@@
-        return pack("H", crc)
+        return pack("<H", crc)

231-239: Make header packing explicitly little-endian.

Avoid platform-dependent native endianness.

-        s = pack(
-            "BBHI4s",
+        s = pack(
+            "<BBHI4s",

410-416: Broaden timestamp type and avoid localtime drift.

Accept int and compute seconds in UTC to prevent TZ skew.

-    def timestamp(self, t: datetime | float) -> float:
+    def timestamp(self, t: datetime | int | float) -> float:
@@
-        if isinstance(t, datetime):
-            t = time.mktime(t.timetuple())
+        if isinstance(t, datetime):
+            # seconds since epoch in UTC
+            t = t.replace(tzinfo=None).timestamp()
garminconnect/__init__.py (1)

541-556: Add date validation for range-based endpoints.

For consistency with other getters, validate start/end dates.

 def get_body_composition(
     self, startdate: str, enddate: str | None = None
 ) -> dict[str, Any]:
@@
-    if enddate is None:
-        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate or startdate, "enddate")
@@
-    params = {"startDate": str(startdate), "endDate": str(enddate)}
+    params = {"startDate": startdate, "endDate": enddate}
 def get_body_battery(
     self, startdate: str, enddate: str | None = None
 ) -> list[dict[str, Any]]:
@@
-    if enddate is None:
-        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate or startdate, "enddate")
@@
-    params = {"startDate": str(startdate), "endDate": str(enddate)}
+    params = {"startDate": startdate, "EndDate": enddate}
 def get_blood_pressure(
     self, startdate: str, enddate: str | None = None
 ) -> dict[str, Any]:
@@
-    if enddate is None:
-        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate or startdate, "enddate")

Also applies to: 720-734, 780-793

♻️ Duplicate comments (11)
pyproject.toml (2)

75-86: Enable Bandit rules or drop S101 ignore.

Per-file ignore lists S101 but "S" isn’t enabled; enable "S" to make S101 valid.

 [tool.ruff.lint]
 select = [
@@
-    "SIM", # flake8-simplify
+    "SIM", # flake8-simplify
+    "S",   # flake8-bandit
 ]
 
 [tool.ruff.lint.per-file-ignores]
 "tests/*" = ["ARG", "S101"]

Also applies to: 93-95


68-71: Exclude non-distribution files from wheels/sdists.

Avoid shipping fixtures and CI files.

 [tool.pdm]
 distribution = true
+
+[tool.pdm.build]
+excludes = [
+  "test_data/**",
+  ".github/**",
+]
README.md (4)

28-31: Fix heading level and add surrounding blank lines (MD001/MD022).

-### API Coverage Statistics
+## API Coverage Statistics
+
 - **Total API Methods**: 101 unique endpoints

86-92: Clarify OAuth example to avoid secret scanner noise and add blanks around fence.

-**Advanced Configuration:**
-```python
-# Optional: Custom OAuth consumer (before login)
-import garth
-garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'}
-```
+**Advanced Configuration:**
+
+```python
+# Optional: Custom OAuth consumer (before login)
+import os
+import garth
+garth.sso.OAUTH_CONSUMER = {
+    "key": os.getenv("GARTH_OAUTH_KEY", "<YOUR_KEY>"),
+    "secret": os.getenv("GARTH_OAUTH_SECRET", "<YOUR_SECRET>"),
+}
+```

96-103: Remove duplicate “Testing” heading and ensure blanks around fences (MD024/MD031).

-## 🧪 Testing
-
-Run the test suite to verify functionality:
-
-## 🧪 Testing
-
-Run the test suite to verify functionality:
+## 🧪 Testing
+
+Run the test suite to verify functionality:

Also applies to: 100-103


252-259: Avoid stale hard-coded dates in examples.

-# Get today's stats
-stats = client.get_stats('2023-08-31')
+# Get today's stats
+from datetime import date
+today = date.today().strftime('%Y-%m-%d')
+stats = client.get_stats(today)
@@
-# Get heart rate data
-hr_data = client.get_heart_rates('2023-08-31')
+# Get heart rate data
+hr_data = client.get_heart_rates(today)
garminconnect/__init__.py (5)

939-961: Use shared timestamp/date format constants to avoid drift.

-            timestamp = datetime.strftime(raw_ts, "%Y-%m-%dT%H:%M:%S.%f")
+            timestamp = datetime.strftime(raw_ts, TIMESTAMP_FORMAT_STR)
@@
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+                raw_ts = datetime.strptime(timestamp, TIMESTAMP_FORMAT_STR)
-            "dateTimestamp": dt.isoformat()[:19] + ".00",
-            "gmtTimestamp": dtGMT.isoformat()[:19] + ".00",
+            "dateTimestamp": dt.strftime(TIMESTAMP_FORMAT_STR)[:-3],
+            "gmtTimestamp": dtGMT.strftime(TIMESTAMP_FORMAT_STR)[:-3],
-            "dateTimestamp": dt.isoformat()[:19] + ".00",  # Local time
-            "gmtTimestamp": dtGMT.isoformat()[:19] + ".00",  # GMT/UTC time
+            "dateTimestamp": dt.strftime(TIMESTAMP_FORMAT_STR)[:-3],   # Local time
+            "gmtTimestamp": dtGMT.strftime(TIMESTAMP_FORMAT_STR)[:-3], # GMT/UTC time
-            "measurementTimestampLocal": dt.isoformat()[:19] + ".00",
-            "measurementTimestampGMT": dtGMT.isoformat()[:19] + ".00",
+            "measurementTimestampLocal": dt.strftime(TIMESTAMP_FORMAT_STR)[:-3],
+            "measurementTimestampGMT": dtGMT.strftime(TIMESTAMP_FORMAT_STR)[:-3],

Also applies to: 622-624, 653-655, 766-768


270-287: Brittle error mapping via string matching; use HTTP status codes.

     def connectapi(self, path: str, **kwargs: Any) -> Any:
         """Wrapper for garth connectapi with error handling."""
         try:
             return self.garth.connectapi(path, **kwargs)
         except Exception as e:
-            logger.error(f"API call failed for path '{path}': {e}")
-            # Re-raise with more context but preserve original exception type
-            if "auth" in str(e).lower() or "401" in str(e):
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.error("API call failed for path '%s': %s%s", path, e, f" (status={status})" if status else "")
+            if status == 401:
                 raise GarminConnectAuthenticationError(
-                    f"Authentication failed: {e}"
+                    f"Authentication failed: {e}"
                 ) from e
-            elif "429" in str(e) or "rate" in str(e).lower():
+            elif status == 429:
                 raise GarminConnectTooManyRequestsError(
                     f"Rate limit exceeded: {e}"
                 ) from e
             else:
                 raise GarminConnectConnectionError(f"Connection error: {e}") from e

288-295: download(): align error handling with connectapi().

     def download(self, path: str, **kwargs: Any) -> Any:
         """Wrapper for garth download with error handling."""
         try:
             return self.garth.download(path, **kwargs)
         except Exception as e:
-            logger.error(f"Download failed for path '{path}': {e}")
-            raise GarminConnectConnectionError(f"Download error: {e}") from e
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.error("Download failed for path '%s': %s%s", path, e, f" (status={status})" if status else "")
+            if status == 401:
+                raise GarminConnectAuthenticationError(f"Download auth failed: {e}") from e
+            if status == 429:
+                raise GarminConnectTooManyRequestsError(f"Download rate-limited: {e}") from e
+            raise GarminConnectConnectionError(f"Download error: {e}") from e

390-395: login(): non-auth failures should not raise AuthenticationError.

-        except Exception as e:
-            if isinstance(e, GarminConnectAuthenticationError):
-                raise
-            else:
-                logger.error(f"Login failed: {e}")
-                raise GarminConnectAuthenticationError(f"Login failed: {e}") from e
+        except Exception as e:
+            if isinstance(e, GarminConnectAuthenticationError):
+                raise
+            logger.exception("Login failed")
+            raise GarminConnectConnectionError(f"Login failed: {e}") from e

1919-1936: upload_workout(): validate parsed payload type.

         if isinstance(workout_json, str):
             import json as _json
 
             try:
                 payload = _json.loads(workout_json)
             except Exception as e:
                 raise ValueError(f"Invalid workout_json string: {e}") from e
         else:
             payload = workout_json
+        if not isinstance(payload, (dict, list)):
+            raise ValueError("workout_json must be a JSON object or array")
         return self.garth.post("connectapi", url, json=payload, api=True)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0a36537 and 52ca923.

📒 Files selected for processing (7)
  • Makefile (0 hunks)
  • README.md (1 hunks)
  • garminconnect/__init__.py (60 hunks)
  • garminconnect/fit.py (13 hunks)
  • pyproject.toml (4 hunks)
  • tests/conftest.py (3 hunks)
  • tests/test_garmin.py (1 hunks)
💤 Files with no reviewable changes (1)
  • Makefile
🧰 Additional context used
📓 Path-based instructions (1)
tests/**

⚙️ CodeRabbit configuration file

tests/**: - test functions shouldn't have a return type hint

  • it's ok to use assert instead of pytest.assume()

Files:

  • tests/test_garmin.py
  • tests/conftest.py
🧠 Learnings (1)
📚 Learning: 2025-04-09T21:31:34.046Z
Learnt from: pedrorijo91
PR: cyberjunky/python-garminconnect#252
File: garminconnect/__init__.py:1373-1373
Timestamp: 2025-04-09T21:31:34.046Z
Learning: Type annotations aren't consistently applied throughout the python-garminconnect codebase, so adding them isn't required to match the existing code style.

Applied to files:

  • tests/test_garmin.py
  • garminconnect/fit.py
🧬 Code graph analysis (2)
tests/test_garmin.py (2)
garminconnect/__init__.py (6)
  • Garmin (84-1982)
  • login (296-395)
  • get_steps_data (451-467)
  • get_daily_steps (485-502)
  • download_activity (1740-1764)
  • upload_activity (1507-1559)
tests/conftest.py (1)
  • vcr (10-12)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~3-~3: Use correct spacing
Context: ...o 11 categories for easy navigation: bash $ ./example.py 🏃‍♂️ 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 [q] Exit program Make your selection: ### API Coverage Statistics - **Total API M...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~28-~28: Use correct spacing
Context: ...ction: ``` ### API Coverage Statistics - Total API Methods: 101 unique endpoint...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~44-~44: Use correct spacing
Context: ...gout, GraphQL) ### Interactive Features - Enhanced User Experience: Categorized ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~50-~50: Use correct spacing
Context: ... export functionality for all data types Donate via PayPal Sponsor on GitHub A comprehensive Python 3 API wrapper for...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~55-~55: Use correct spacing
Context: ...ess to health, fitness, and device data. ## 📖 About This library enables developer...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a problem here.
Context: ... programmatically access Garmin Connect data including: - Health Metrics: Heart rate, sleep, str...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~65-~65: Use correct spacing
Context: ...s, progress tracking, date range queries Compatible with all Garmin Connect accou...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~67-~67: There might be a mistake here.
Context: ...counts. See https://connect.garmin.com/ ## 📦 Installation Install from PyPI: ```...

(QB_NEW_EN_OTHER)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~71-~71: Use correct spacing
Context: ... ## 📦 Installation Install from PyPI: bash pip3 install garminconnect ## 🔐 Authentication The library uses the ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~77-~77: There might be a mistake here.
Context: ... garminconnect ``` ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~79-~79: Use correct spacing
Context: ...Garth. Key Features: - Login credentials vali...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~81-~81: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~82-~82: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authe...

(QB_NEW_EN)


[grammar] ~83-~83: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~84-~84: Use correct spacing
Context: ...Same authentication flow as official app Advanced Configuration: ```python # Op...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~86-~86: There might be a problem here.
Context: ... official app Advanced Configuration: python # Optional: Custom OAuth consumer (before login) import garth garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'} Token Storage: Tokens are automatically sav...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~93-93: There might be a mistake here.
Context: ...: 'your_secret'} ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~94-~94: Use articles correctly
Context: ...orage:** Tokens are automatically saved to ~/.garminconnect directory for persis...

(QB_NEW_EN_OTHER_ERROR_IDS_11)


[grammar] ~94-~94: Use correct spacing
Context: ...directory for persistent authentication. ## 🧪 Testing Run the test suite to verify...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~96-~96: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~98-~98: Use correct spacing
Context: ... the test suite to verify functionality: ## 🧪 Testing Run the test suite to verify...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~100-~100: There might be a mistake here.
Context: ... to verify functionality: ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~102-~102: Use correct spacing
Context: ... the test suite to verify functionality: Prerequisites: ```bash # Set token dir...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~104-~104: Use correct spacing
Context: ... verify functionality: Prerequisites: bash # Set token directory (uses example.py credentials) export GARMINTOKENS=~/.garminconnect # Install development dependencies pdm install --group :all Run Tests: ```bash pdm run test ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~113-~113: Use correct spacing
Context: ...m install --group :all **Run Tests:**bash pdm run test # Run all tests pdm run testcov # Run tests with coverage report ``` Note: Test files use credential tokens...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~119-~119: Use correct spacing
Context: ...first to generate authentication tokens. ## 🛠️ Development Set up a development en...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~121-~121: There might be a mistake here.
Context: ...thentication tokens. ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~123-~123: Use correct spacing
Context: ...evelopment environment for contributing: > Note: This project uses [PDM](https://...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~125-~125: Use correct spacing
Context: ...ured as PDM scripts in pyproject.toml. Environment Setup: ```bash # Install P...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~127-~127: Use correct spacing
Context: ... pyproject.toml. Environment Setup: bash # Install PDM (Python Dependency Manager) pip install pdm # Install all development dependencies pdm install --group :all # Install pre-commit hooks (optional) pre-commit install --install-hooks Available Development Commands: ```bas...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~139-~139: Use correct spacing
Context: ...s **Available 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 test # Run test suite pdm run testcov # Run tests with coverage report pdm run all # Run full quality checks (lint + 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 **View all available commands:**bash p...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~151-~151: Use correct spacing
Context: ...PyPI **View all available commands:**bash pdm run --list # Display all available PDM scripts **Code Quality Workflow:**bash # Befor...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~156-~156: Use correct spacing
Context: ...DM scripts **Code Quality Workflow:**bash # Before making changes pdm run lint # Check current code quality # After making changes pdm run format # Auto-format your code pdm run lint # Verify code quality pdm run test # Run tests to ensure nothing broke ``` Run these commands before submitting PRs...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~167-~167: Use correct spacing
Context: ...ng PRs to ensure code quality standards. ## 📦 Publishing For package maintainers: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~169-~169: There might be a mistake here.
Context: ...ode quality standards. ## 📦 Publishing For package maintainers: ## 📦 Publishi...

(QB_NEW_EN)


[grammar] ~171-~171: Use correct spacing
Context: ... 📦 Publishing For package maintainers: ## 📦 Publishing For package maintainers: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~173-~173: There might be a mistake here.
Context: ...r package maintainers: ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~175-~175: There might be a problem here.
Context: ... 📦 Publishing For package maintainers: Setup PyPI credentials: ```bash pip install...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~177-~177: Use correct spacing
Context: ... maintainers: Setup PyPI credentials: bash pip install twine vi ~/.pypirc ini [pypi] username = __token__ password = <PyPI_API_TOKEN> Publish new version: ```bash pdm run p...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~188-~188: Use correct spacing
Context: ...I_API_TOKEN> **Publish new version:**bash pdm run publish # Build and publish to PyPI **Alternative publishing steps:**bash ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~193-~193: Use correct spacing
Context: ...yPI **Alternative publishing steps:**bash pdm run build # Build package only pdm publish # Publish pre-built package ``` ## 🤝 Contributing We welcome contribution...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~199-~199: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~201-~201: Use correct spacing
Context: ... contributions! Here's how you can help: - Report Issues: Bug reports and feature...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~203-~203: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~204-~204: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~205-~205: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~206-~206: Use correct spacing
Context: ...prove examples, add use cases, fix typos Before Contributing: 1. Set up develop...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~208-~208: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~209-~209: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~209-~209: There might be a mistake here.
Context: ...environment (pdm install --group :all) 2. Execute code quality checks (`pdm run fo...

(QB_NEW_EN_OTHER)


[grammar] ~212-~212: There might be a mistake here.
Context: ... Follow existing code style and patterns Development Workflow: ```bash # 1. Set...

(QB_NEW_EN_OTHER)


[grammar] ~214-~214: Use correct spacing
Context: ...le and patterns Development Workflow: bash # 1. Setup environment pdm install --group :all # 2. Make your changes # ... edit code ... # 3. Quality checks pdm run format # Auto-format code pdm run lint # Check code quality pdm run test # Run tests # 4. Submit PR git commit -m "Your changes" git push origin your-branch ## 💻 Usage Examples ### Interactive Demo ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~232-~232: There might be a mistake here.
Context: ...in your-branch ``` ## 💻 Usage Examples ### Interactive Demo Run the comprehensive A...

(QB_NEW_EN)


[grammar] ~234-~234: There might be a mistake here.
Context: ... 💻 Usage Examples ### Interactive Demo Run the comprehensive API demonstration:...

(QB_NEW_EN)


[grammar] ~235-~235: Use correct spacing
Context: ...Run the comprehensive API demonstration: bash pdm install --group example # Install example dependencies ./example.py ### Jupyter Notebook Explore the API interac...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~241-~241: There might be a mistake here.
Context: ...s ./example.py ``` ### Jupyter Notebook Explore the API interactively with our [...

(QB_NEW_EN)


[grammar] ~242-~242: Use correct spacing
Context: ...minconnect/blob/master/reference.ipynb). ### Python Code Examples ```python from garm...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~244-~244: Use correct spacing
Context: ...erence.ipynb). ### Python Code Examples python from garminconnect import Garmin # Initialize and login client = Garmin('your_email', 'your_password') client.login() # Get today's stats stats = client.get_stats('2023-08-31') print(f"Steps: {stats['totalSteps']}") # Get heart rate data hr_data = client.get_heart_rates('2023-08-31') print(f"Resting HR: {hr_data['restingHeartRate']}") ### Additional Resources - Source Code: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~261-~261: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~262-~262: Use an accepted time format
Context: ...Resources - Source Code: example.py - API Documentation: Comprehensive metho...

(QB_NEW_EN_OTHER_ERROR_IDS_4)


[grammar] ~263-~263: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~264-~264: Use correct spacing
Context: ...rld usage examples in tests/ directory ## 🙏 Acknowledgments Special thanks to al...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~266-~266: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~268-~268: Use correct spacing
Context: ...rs who have helped improve this project: - Community Contributors: Bug reports, f...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~270-~270: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~271-~271: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~272-~272: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~273-~273: Use correct spacing
Context: ...rs**: Improving examples and user guides This project thrives thanks to community...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~275-~275: Use correct spacing
Context: ...s to community involvement and feedback. ## 💖 Support This Project If you find thi...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~277-~277: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~279-~279: Use correct spacing
Context: ...s continued development and maintenance: ### 🌟 Ways to Support - **⭐ Star this repo...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~281-~281: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[typographical] ~283-~283: To join two clauses or set off examples, consider using an em dash.
Context: ...s to Support - ⭐ Star this repository - Help others discover the project - **💰 Fina...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~283-~283: There might be a mistake here.
Context: ...ory** - Help others discover the project - 💰 Financial Support - Contribute to d...

(QB_NEW_EN_OTHER)


[typographical] ~284-~284: To join two clauses or set off examples, consider using an em dash.
Context: ...ver the project - 💰 Financial Support - Contribute to development and hosting costs - **🐛...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~284-~284: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[typographical] ~285-~285: To join two clauses or set off examples, consider using an em dash.
Context: ...t and hosting costs - 🐛 Report Issues - Help improve stability and compatibility -...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~285-~285: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[typographical] ~286-~286: To join two clauses or set off examples, consider using an em dash.
Context: ...d compatibility - 📖 Spread the Word - Share with other developers ### 💳 Financial...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~286-~286: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~288-~288: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~294-~294: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~295-~295: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~297-~297: Use correct spacing
Context: ...ion for hundreds of hours of development Every contribution, no matter the size, ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~299-~299: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

28-28: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


82-82: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


83-83: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


87-87: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


100-100: Multiple headings with the same content

(MD024, no-duplicate-heading)


105-105: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


114-114: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


128-128: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


140-140: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


152-152: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


157-157: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


173-173: Multiple headings with the same content

(MD024, no-duplicate-heading)


178-178: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


181-181: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


182-182: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


189-189: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


194-194: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


209-209: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


210-210: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


215-215: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


234-234: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


236-236: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


241-241: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


244-244: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


245-245: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


261-261: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


262-262: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


294-294: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

🔇 Additional comments (2)
garminconnect/__init__.py (1)

1896-1896: Ensure return types for methods using garth.post/put/request match their annotations
Methods annotated to return dict[str, Any] (e.g. get_weigh_ins, get_hydration_data, get_workouts, etc.) currently return the raw self.garth.post/.put/.request result, not the parsed JSON. Either call .json() on those responses or change the return‐type annotations to the actual response type (e.g. requests.Response).

tests/test_garmin.py (1)

9-10: Use a syntactically valid email to avoid login validation failures without GARMINTOKENS

If GARMINTOKENS isn’t set locally, login() will reject usernames without '@'. Use a valid-looking email to keep tests runnable for contributors.

 def garmin() -> garminconnect.Garmin:
-    return garminconnect.Garmin("email", "password")
+    return garminconnect.Garmin("[email protected]", "password")

Comment on lines +210 to 219
def __str__(self) -> str:
orig_pos = self.buf.tell()
self.buf.seek(0)
lines = []
while True:
b = self.buf.read(16)
if not b:
break
lines.append(" ".join(["%02x" % ord(c) for c in b]))
lines.append(" ".join([f"{ord(c):02x}" for c in b]))
self.buf.seek(orig_pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Hex dump uses ord() on ints (TypeError).

Iterating bytes yields ints; ord() is invalid.

-            lines.append(" ".join([f"{ord(c):02x}" for c in b]))
+            lines.append(" ".join(f"{byte:02x}" for byte in b))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __str__(self) -> str:
orig_pos = self.buf.tell()
self.buf.seek(0)
lines = []
while True:
b = self.buf.read(16)
if not b:
break
lines.append(" ".join(["%02x" % ord(c) for c in b]))
lines.append(" ".join([f"{ord(c):02x}" for c in b]))
self.buf.seek(orig_pos)
def __str__(self) -> str:
orig_pos = self.buf.tell()
self.buf.seek(0)
lines = []
while True:
b = self.buf.read(16)
if not b:
break
lines.append(" ".join(f"{byte:02x}" for byte in b))
self.buf.seek(orig_pos)
return "\n".join(lines)
🤖 Prompt for AI Agents
In garminconnect/fit.py around lines 210 to 219, the hex dump loop incorrectly
calls ord() on items from a bytes object (iterating bytes yields ints), causing
a TypeError; replace the list comprehension to format each byte integer directly
(e.g., use f"{c:02x}" for c in b) so the code builds the hex strings from the
integer byte values, keep the rest of the logic unchanged (seek, read, append,
restore position).

Comment on lines +392 to 396
def finish(self) -> None:
"""re-weite file-header, then append crc to end of file"""
data_size = self.get_size() - self.HEADER_SIZE
self.write_header(data_size=data_size)
crc = self.crc()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Typo in docstring (“re-weite”).

Minor spelling fix.

-        """re-weite file-header, then append crc to end of file"""
+        """Re-write file header, then append CRC to end of file."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def finish(self) -> None:
"""re-weite file-header, then append crc to end of file"""
data_size = self.get_size() - self.HEADER_SIZE
self.write_header(data_size=data_size)
crc = self.crc()
def finish(self) -> None:
"""Re-write file header, then append CRC to end of file."""
data_size = self.get_size() - self.HEADER_SIZE
self.write_header(data_size=data_size)
crc = self.crc()
🤖 Prompt for AI Agents
In garminconnect/fit.py around lines 392 to 396, the docstring for finish()
contains a typo ("re-weite file-header"); update the docstring to correct the
spelling (e.g., "re-write file header, then append CRC to end of file") and
ensure casing/spacing is consistent with project style while leaving the
function logic unchanged.

Comment on lines 46 to 55
daily_steps_data = garmin.get_daily_steps(DATE, DATE)
# The API returns a dict, likely with a list inside
if isinstance(daily_steps_data, dict) and len(daily_steps_data) > 0:
# Get the first available data entry
daily_steps = (
list(daily_steps_data.values())[0] if daily_steps_data else daily_steps_data
)
else:
daily_steps = daily_steps_data
assert "calendarDate" in daily_steps or "totalSteps" in daily_steps
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make daily_steps extraction deterministic and type-safe

Taking the “first dict value” is order-dependent and may return a list. Extract the first list of entries explicitly and validate.

-    daily_steps_data = garmin.get_daily_steps(DATE, DATE)
-    # The API returns a dict, likely with a list inside
-    if isinstance(daily_steps_data, dict) and len(daily_steps_data) > 0:
-        # Get the first available data entry
-        daily_steps = (
-            list(daily_steps_data.values())[0] if daily_steps_data else daily_steps_data
-        )
-    else:
-        daily_steps = daily_steps_data
-    assert "calendarDate" in daily_steps or "totalSteps" in daily_steps
+    daily_steps_data = garmin.get_daily_steps(DATE, DATE)
+    entry = None
+    if isinstance(daily_steps_data, dict):
+        for v in daily_steps_data.values():
+            if isinstance(v, list) and v:
+                entry = v[0]
+                break
+        if entry is None:
+            pytest.skip("No daily steps entries found")
+    elif isinstance(daily_steps_data, list):
+        if not daily_steps_data:
+            pytest.skip("No daily steps data")
+        entry = daily_steps_data[0]
+    else:
+        entry = daily_steps_data
+    assert isinstance(entry, dict)
+    assert ("calendarDate" in entry) or ("totalSteps" in entry)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
daily_steps_data = garmin.get_daily_steps(DATE, DATE)
# The API returns a dict, likely with a list inside
if isinstance(daily_steps_data, dict) and len(daily_steps_data) > 0:
# Get the first available data entry
daily_steps = (
list(daily_steps_data.values())[0] if daily_steps_data else daily_steps_data
)
else:
daily_steps = daily_steps_data
assert "calendarDate" in daily_steps or "totalSteps" in daily_steps
daily_steps_data = garmin.get_daily_steps(DATE, DATE)
entry = None
if isinstance(daily_steps_data, dict):
for v in daily_steps_data.values():
if isinstance(v, list) and v:
entry = v[0]
break
if entry is None:
pytest.skip("No daily steps entries found")
elif isinstance(daily_steps_data, list):
if not daily_steps_data:
pytest.skip("No daily steps data")
entry = daily_steps_data[0]
else:
entry = daily_steps_data
assert isinstance(entry, dict)
assert ("calendarDate" in entry) or ("totalSteps" in entry)
🤖 Prompt for AI Agents
In tests/test_garmin.py around lines 46 to 55, the test currently grabs "the
first dict value" which is order-dependent and may return a list; change it to
explicitly handle the expected structures: if daily_steps_data is a dict, find
the first value that is a non-empty list and use its first entry; if
daily_steps_data is already a list, use its first entry; if it's a dict
representing a single entry, use it directly; finally assert that the chosen
entry is a dict and contains "calendarDate" or "totalSteps". Ensure you validate
types and handle empty results by failing the test with a clear message.

Comment on lines 123 to 127
def test_download_activity(garmin: garminconnect.Garmin) -> None:
garmin.login()
activity_id = "11998957007"
activity = garmin.download_activity(activity_id)
assert activity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Strengthen the assertion for download payload

Assert type/size for clearer failures.

-    activity = garmin.download_activity(activity_id)
-    assert activity
+    activity = garmin.download_activity(activity_id)
+    assert isinstance(activity, (bytes, bytearray)) and len(activity) > 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_download_activity(garmin: garminconnect.Garmin) -> None:
garmin.login()
activity_id = "11998957007"
activity = garmin.download_activity(activity_id)
assert activity
def test_download_activity(garmin: garminconnect.Garmin) -> None:
garmin.login()
activity_id = "11998957007"
- activity = garmin.download_activity(activity_id)
activity = garmin.download_activity(activity_id)
assert isinstance(activity, (bytes, bytearray)) and len(activity) > 0
🤖 Prompt for AI Agents
In tests/test_garmin.py around lines 123 to 127, the test only asserts that
activity is truthy which gives poor failure detail; update the assertion to
validate the expected payload type and non-empty content (for example assert
isinstance(activity, dict) and that key fields exist or assert
isinstance(activity, (bytes, str)) and len(activity) > 0) so failures clearly
indicate whether the payload shape or size is wrong.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
garminconnect/__init__.py (3)

136-138: Missing leading slash in service paths (breaks connectapi endpoints).

Both hydration log and pregnancy snapshot URLs lack a leading “/”, likely yielding wrong paths.

-        self.garmin_connect_set_hydration_url = (
-            "usersummary-service/usersummary/hydration/log"
-        )
+        self.garmin_connect_set_hydration_url = (
+            "/usersummary-service/usersummary/hydration/log"
+        )
@@
-        self.garmin_connect_pregnancy_snapshot_url = (
-            "periodichealth-service/menstrualcycle/pregnancysnapshot"
-        )
+        self.garmin_connect_pregnancy_snapshot_url = (
+            "/periodichealth-service/menstrualcycle/pregnancysnapshot"
+        )

Also applies to: 194-196


939-941: Use TIMESTAMP_FORMAT_STR consistently (avoid format drift).

-            timestamp = datetime.strftime(raw_ts, "%Y-%m-%dT%H:%M:%S.%f")
+            timestamp = raw_ts.strftime(TIMESTAMP_FORMAT_STR)
@@
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+                raw_ts = datetime.strptime(timestamp, TIMESTAMP_FORMAT_STR)
@@
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+                raw_ts = datetime.strptime(timestamp, TIMESTAMP_FORMAT_STR)

Also applies to: 956-961, 968-977


749-757: Validate blood pressure inputs (integers, positive).

Prevents silent bad requests and aligns with other validators.

     def set_blood_pressure(
         self,
         systolic: int,
         diastolic: int,
         pulse: int,
         timestamp: str = "",
         notes: str = "",
     ) -> dict[str, Any]:
@@
-        url = f"{self.garmin_connect_set_blood_pressure_endpoint}"
+        url = f"{self.garmin_connect_set_blood_pressure_endpoint}"
+        systolic = _validate_positive_integer(systolic, "systolic")
+        diastolic = _validate_positive_integer(diastolic, "diastolic")
+        pulse = _validate_positive_integer(pulse, "pulse")
♻️ Duplicate comments (9)
pyproject.toml (3)

75-86: Enable Ruff “S” rules or drop S101 ignore.

per-file-ignores references S101 but “S” isn’t in select. Prefer enabling Bandit rules.

 [tool.ruff.lint]
 select = [
     "E",   # pycodestyle errors
     "W",   # pycodestyle warnings
     "F",   # pyflakes
     "I",   # isort
     "B",   # flake8-bugbear
     "C4",  # flake8-comprehensions
     "UP",  # pyupgrade
     "ARG", # flake8-unused-arguments
     "SIM", # flake8-simplify
+    "S",   # flake8-bandit
 ]
 
 [tool.ruff.lint.per-file-ignores]
 "tests/*" = ["ARG", "S101"]

Also applies to: 93-95


118-119: Run ruff --fix before isort/black to avoid churn.

-format = {composite = ["pdm run isort garminconnect tests", "pdm run black -l 88 garminconnect tests", "pdm run ruff check garminconnect tests --fix"]}
+format = {composite = ["pdm run ruff check garminconnect tests --fix", "pdm run isort garminconnect tests", "pdm run black -l 88 garminconnect tests"]}

68-71: Exclude fixtures and CI files from built distributions.

 [tool.pdm]
 distribution = true
+
+[tool.pdm.build]
+excludes = [
+  "test_data/**",
+  ".github/**",
+]
garminconnect/__init__.py (6)

390-395: Don’t wrap non-auth failures as AuthenticationError; log traceback.

-            else:
-                logger.error(f"Login failed: {e}")
-                raise GarminConnectAuthenticationError(f"Login failed: {e}") from e
+            else:
+                logger.exception("Login failed")
+                raise GarminConnectConnectionError(f"Login failed: {e}") from e

1803-1809: Fix copy/paste log message for HR time-in-zones.

-        logger.debug("Requesting split summaries for activity id %s", activity_id)
+        logger.debug("Requesting HR time-in-zones for activity id %s", activity_id)

1927-1936: Validate upload_workout payload type after parsing.

         if isinstance(workout_json, str):
             import json as _json
 
             try:
                 payload = _json.loads(workout_json)
             except Exception as e:
                 raise ValueError(f"Invalid workout_json string: {e}") from e
         else:
             payload = workout_json
+        if not isinstance(payload, (dict, list)):
+            raise ValueError("workout_json must be a JSON object or array")
         return self.garth.post("connectapi", url, json=payload, api=True)

1897-1904: get_workouts: end is treated as limit (logic bug).

Use limit = end - start (or change signature to limit). Current code sends “limit=end”.

-    def get_workouts(self, start: int = 0, end: int = 100) -> dict[str, Any]:
+    def get_workouts(self, start: int = 0, end: int = 100) -> dict[str, Any]:
         """Return workouts from start till end."""
 
         url = f"{self.garmin_workouts}/workouts"
-        logger.debug(f"Requesting workouts from {start}-{end}")
-        params = {"start": start, "limit": end}
+        if end < start:
+            raise ValueError("end must be >= start")
+        limit = end - start
+        logger.debug(f"Requesting workouts from {start}-{end} (limit={limit})")
+        params = {"start": start, "limit": limit}
         return self.connectapi(url, params=params)

270-287: Classify HTTP failures by status; log stack traces.

String-matching on “auth/401/429” is brittle. Inspect response.status_code and use logger.exception for trace.

     def connectapi(self, path: str, **kwargs: Any) -> Any:
         """Wrapper for garth connectapi with error handling."""
         try:
             return self.garth.connectapi(path, **kwargs)
         except Exception as e:
-            logger.error(f"API call failed for path '{path}': {e}")
-            # Re-raise with more context but preserve original exception type
-            if "auth" in str(e).lower() or "401" in str(e):
-                raise GarminConnectAuthenticationError(
-                    f"Authentication failed: {e}"
-                ) from e
-            elif "429" in str(e) or "rate" in str(e).lower():
-                raise GarminConnectTooManyRequestsError(
-                    f"Rate limit exceeded: {e}"
-                ) from e
-            else:
-                raise GarminConnectConnectionError(f"Connection error: {e}") from e
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.exception(
+                "API call failed for path '%s'%s",
+                path,
+                f" (status={status})" if status else "",
+            )
+            if status == 401:
+                raise GarminConnectAuthenticationError(f"Authentication failed: {e}") from e
+            if status == 429:
+                raise GarminConnectTooManyRequestsError(f"Rate limit exceeded: {e}") from e
+            raise GarminConnectConnectionError(f"Connection error: {e}") from e

288-295: Align download() error handling with connectapi().

     def download(self, path: str, **kwargs: Any) -> Any:
         """Wrapper for garth download with error handling."""
         try:
             return self.garth.download(path, **kwargs)
         except Exception as e:
-            logger.error(f"Download failed for path '{path}': {e}")
-            raise GarminConnectConnectionError(f"Download error: {e}") from e
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.exception(
+                "Download failed for path '%s'%s",
+                path,
+                f" (status={status})" if status else "",
+            )
+            if status == 401:
+                raise GarminConnectAuthenticationError(f"Download failed: {e}") from e
+            if status == 429:
+                raise GarminConnectTooManyRequestsError(f"Download failed: {e}") from e
+            raise GarminConnectConnectionError(f"Download error: {e}") from e
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 52ca923 and b3e8608.

📒 Files selected for processing (3)
  • .pre-commit-config.yaml (1 hunks)
  • garminconnect/__init__.py (60 hunks)
  • pyproject.toml (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.gitignore (1)

54-67: Ignore common tool caches and coverage artifacts.

Add .ruff_cache/ and coverage/ to avoid committing local analysis/cache output and coverage reports (your pdm script writes coverage/coverage.xml).

 # Unit test / coverage reports
 htmlcov/
+.ruff_cache/
 .tox/
 .nox/
 .coverage
 .coverage.*
 .cache
 nosetests.xml
 coverage.xml
+coverage/
 *.cover
 *.py,cover
 .hypothesis/
 .pytest_cache/

Also applies to: 138-145

♻️ Duplicate comments (6)
.gitignore (1)

121-121: Remove duplicate .venv ignore entry (keep the trailing-slash form).

Retain only ".venv/" for consistency; drop the plain ".venv".

 # Environments
 .env
 .envrc
-.venv
 env/
 venv/
 ENV/
 env.bak/
 venv.bak/
pyproject.toml (3)

75-86: Enable Bandit rules in Ruff or drop the S101 ignore.

Per-file-ignores reference S101 but "S" isn’t selected. Prefer enabling security checks.

 [tool.ruff.lint]
 select = [
     "E",   # pycodestyle errors
     "W",   # pycodestyle warnings
     "F",   # pyflakes
     "I",   # isort
     "B",   # flake8-bugbear
     "C4",  # flake8-comprehensions
     "UP",  # pyupgrade
     "ARG", # flake8-unused-arguments
     "SIM", # flake8-simplify
+    "S",   # flake8-bandit
 ]
 
 [tool.ruff.lint.per-file-ignores]
 "tests/*" = ["ARG", "S101"]

Also applies to: 93-95


115-120: Run Ruff autofixes before isort/black to reduce churn.

Order matters to avoid rework.

 [tool.pdm.scripts]
 # Development workflow
 install = "pdm install --group :all"
-format = {composite = ["pdm run isort garminconnect tests", "pdm run black -l 88 garminconnect tests", "pdm run ruff check garminconnect tests --fix"]}
+format = {composite = ["pdm run ruff check garminconnect tests --fix", "pdm run isort garminconnect tests", "pdm run black -l 88 garminconnect tests"]}
 lint = {composite = ["pdm run isort --check-only garminconnect tests", "pdm run ruff check garminconnect tests", "pdm run black -l 88 garminconnect tests --check --diff", "pdm run mypy garminconnect tests"]}

68-71: Exclude test fixtures and CI files from built artifacts.

Avoid shipping test_data and CI config in wheels/sdists.

 [tool.pdm]
 distribution = true
+
+[tool.pdm.build]
+excludes = [
+  "test_data/**",
+  ".github/**",
+]
README.md (2)

79-95: Clarify OAuth example to avoid secret-scanner noise and show env-var usage.

Label placeholders and source from env; ensure blank lines around fences.

 ## 🔐 Authentication
 
 The library uses the same OAuth authentication as the official Garmin Connect app via [Garth](https://github.com/matin/garth).
 
 **Key Features:**
 - Login credentials valid for one year (no repeated logins)
 - Secure OAuth token storage 
 - Same authentication flow as official app
 
 **Advanced Configuration:**
+
 ```python
-# Optional: Custom OAuth consumer (before login)
-import garth
-garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'}
+# Optional: Custom OAuth consumer (before login)
+import os
+import garth
+garth.sso.OAUTH_CONSUMER = {
+    "key": os.getenv("GARTH_OAUTH_KEY", "<YOUR_KEY>"),
+    "secret": os.getenv("GARTH_OAUTH_SECRET", "<YOUR_SECRET>"),
+}

Token Storage:
-Tokens are automatically saved to ~/.garminconnect directory for persistent authentication.
+Tokens are automatically saved to the ~/.garminconnect directory for persistent authentication.


---

`254-261`: **Avoid stale hard-coded dates in examples.**

Compute today’s date for stats/HR examples.


```diff
-# Get today's stats
-stats = client.get_stats('2023-08-31')
+# Get today's stats
+from datetime import date
+today = date.today().strftime("%Y-%m-%d")
+stats = client.get_stats(today)
 print(f"Steps: {stats['totalSteps']}")
 
 # Get heart rate data
-hr_data = client.get_heart_rates('2023-08-31')
+hr_data = client.get_heart_rates(today)
 print(f"Resting HR: {hr_data['restingHeartRate']}")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b3e8608 and b1a7f56.

📒 Files selected for processing (5)
  • .gitignore (2 hunks)
  • README.md (1 hunks)
  • pyproject.toml (5 hunks)
  • requirements-dev.txt (0 hunks)
  • requirements-test.txt (0 hunks)
💤 Files with no reviewable changes (2)
  • requirements-dev.txt
  • requirements-test.txt
🧰 Additional context used
🧬 Code graph analysis (1)
README.md (1)
garminconnect/__init__.py (1)
  • __init__ (19-221)
🪛 LanguageTool
README.md

[grammar] ~3-~3: Use correct spacing
Context: ...o 11 categories for easy navigation: bash $ ./example.py 🏃‍♂️ 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 [q] Exit program Make your selection: ### API Coverage Statistics - **Total API M...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~28-~28: Use correct spacing
Context: ...ction: ``` ### API Coverage Statistics - Total API Methods: 101 unique endpoint...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~44-~44: Use correct spacing
Context: ...gout, GraphQL) ### Interactive Features - Enhanced User Experience: Categorized ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~50-~50: Use correct spacing
Context: ... export functionality for all data types Donate via PayPal Sponsor on GitHub A comprehensive Python 3 API wrapper for...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~55-~55: Use correct spacing
Context: ...ess to health, fitness, and device data. ## 📖 About This library enables developer...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a problem here.
Context: ... programmatically access Garmin Connect data including: - Health Metrics: Heart rate, sleep, str...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~65-~65: Use correct spacing
Context: ...s, progress tracking, date range queries Compatible with all Garmin Connect accou...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~67-~67: There might be a mistake here.
Context: ...counts. See https://connect.garmin.com/ ## 📦 Installation Install from PyPI: ```...

(QB_NEW_EN_OTHER)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~71-~71: Use correct spacing
Context: ... ## 📦 Installation Install from PyPI: bash pip3 install garminconnect ## 🔐 Authentication The library uses the ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~77-~77: There might be a mistake here.
Context: ... garminconnect ``` ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~79-~79: Use correct spacing
Context: ...Garth. Key Features: - Login credentials vali...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~81-~81: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~82-~82: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authe...

(QB_NEW_EN)


[grammar] ~83-~83: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~84-~84: Use correct spacing
Context: ...Same authentication flow as official app Advanced Configuration: ```python # Op...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~86-~86: There might be a problem here.
Context: ... official app Advanced Configuration: python # Optional: Custom OAuth consumer (before login) import garth garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'} Token Storage: Tokens are automatically sav...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~93-93: There might be a mistake here.
Context: ...: 'your_secret'} ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~94-~94: Use articles correctly
Context: ...orage:** Tokens are automatically saved to ~/.garminconnect directory for persis...

(QB_NEW_EN_OTHER_ERROR_IDS_11)


[grammar] ~94-~94: Use correct spacing
Context: ...directory for persistent authentication. ## 🧪 Testing Run the test suite to verify...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~96-~96: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~98-~98: Use correct spacing
Context: ... the test suite to verify functionality: ## 🧪 Testing Run the test suite to verify...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~100-~100: There might be a mistake here.
Context: ... to verify functionality: ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~102-~102: Use correct spacing
Context: ... the test suite to verify functionality: Prerequisites: ```bash # Set token dir...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~104-~104: Use correct spacing
Context: ... verify functionality: Prerequisites: bash # Set token directory (uses example.py credentials) export GARMINTOKENS=~/.garminconnect # Install development dependencies pdm install --group :all Run Tests: ```bash pdm run test ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~113-~113: Use correct spacing
Context: ...m install --group :all **Run Tests:**bash pdm run test # Run all tests pdm run testcov # Run tests with coverage report ``` Note: Test files use credential tokens...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~119-~119: Use correct spacing
Context: ...first to generate authentication tokens. ## 🛠️ Development Set up a development en...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~121-~121: There might be a mistake here.
Context: ...thentication tokens. ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~123-~123: Use correct spacing
Context: ...evelopment environment for contributing: > Note: This project uses [PDM](https://...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~125-~125: Use correct spacing
Context: ...ured as PDM scripts in pyproject.toml. Environment Setup: ```bash # Install P...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~127-~127: Use correct spacing
Context: ... pyproject.toml. Environment Setup: bash # Install PDM (Python Dependency Manager) pip install pdm # Install all development dependencies pdm install --group :all # Install pre-commit hooks (optional) pre-commit install --install-hooks Available Development Commands: ```bas...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~139-~139: Use correct spacing
Context: ...s **Available 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 errors in code and comments pdm run test # Run test suite pdm run testcov # Run tests with coverage report pdm run all # Run full quality checks (lint + codespell + 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 **View all available commands:**bash p...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~152-~152: Use correct spacing
Context: ...PyPI **View all available commands:**bash pdm run --list # Display all available PDM scripts **Code Quality Workflow:**bash # Befor...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~157-~157: Use correct spacing
Context: ...DM scripts **Code Quality Workflow:**bash # Before making changes pdm run lint # Check current code quality # After making changes pdm run format # Auto-format your code pdm run lint # Verify code quality pdm run codespell # Check spelling pdm run test # Run tests to ensure nothing broke ``` Run these commands before submitting PRs...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~169-~169: Use correct spacing
Context: ...ng PRs to ensure code quality standards. ## 📦 Publishing For package maintainers: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~171-~171: There might be a mistake here.
Context: ...ode quality standards. ## 📦 Publishing For package maintainers: ## 📦 Publishi...

(QB_NEW_EN)


[grammar] ~173-~173: Use correct spacing
Context: ... 📦 Publishing For package maintainers: ## 📦 Publishing For package maintainers: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~175-~175: There might be a mistake here.
Context: ...r package maintainers: ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~177-~177: There might be a problem here.
Context: ... 📦 Publishing For package maintainers: Setup PyPI credentials: ```bash pip install...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~179-~179: Use correct spacing
Context: ... maintainers: Setup PyPI credentials: bash pip install twine vi ~/.pypirc ini [pypi] username = __token__ password = <PyPI_API_TOKEN> Publish new version: ```bash pdm run p...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~190-~190: Use correct spacing
Context: ...I_API_TOKEN> **Publish new version:**bash pdm run publish # Build and publish to PyPI **Alternative publishing steps:**bash ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~195-~195: Use correct spacing
Context: ...yPI **Alternative publishing steps:**bash pdm run build # Build package only pdm publish # Publish pre-built package ``` ## 🤝 Contributing We welcome contribution...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~201-~201: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~203-~203: Use correct spacing
Context: ... contributions! Here's how you can help: - Report Issues: Bug reports and feature...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~205-~205: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~206-~206: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~207-~207: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~208-~208: Use correct spacing
Context: ...prove examples, add use cases, fix typos Before Contributing: 1. Set up develop...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~210-~210: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~211-~211: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~211-~211: There might be a mistake here.
Context: ...environment (pdm install --group :all) 2. Execute code quality checks (`pdm run fo...

(QB_NEW_EN_OTHER)


[grammar] ~214-~214: There might be a mistake here.
Context: ... Follow existing code style and patterns Development Workflow: ```bash # 1. Set...

(QB_NEW_EN_OTHER)


[grammar] ~216-~216: Use correct spacing
Context: ...le and patterns Development Workflow: bash # 1. Setup environment pdm install --group :all # 2. Make your changes # ... edit code ... # 3. Quality checks pdm run format # Auto-format code pdm run lint # Check code quality pdm run test # Run tests # 4. Submit PR git commit -m "Your changes" git push origin your-branch ## 💻 Usage Examples ### Interactive Demo ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~234-~234: There might be a mistake here.
Context: ...in your-branch ``` ## 💻 Usage Examples ### Interactive Demo Run the comprehensive A...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ... 💻 Usage Examples ### Interactive Demo Run the comprehensive API demonstration:...

(QB_NEW_EN)


[grammar] ~237-~237: Use correct spacing
Context: ...Run the comprehensive API demonstration: bash pdm install --group example # Install example dependencies ./example.py ### Jupyter Notebook Explore the API interac...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~243-~243: There might be a mistake here.
Context: ...s ./example.py ``` ### Jupyter Notebook Explore the API interactively with our [...

(QB_NEW_EN)


[grammar] ~244-~244: Use correct spacing
Context: ...minconnect/blob/master/reference.ipynb). ### Python Code Examples ```python from garm...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~246-~246: Use correct spacing
Context: ...erence.ipynb). ### Python Code Examples python from garminconnect import Garmin # Initialize and login client = Garmin('your_email', 'your_password') client.login() # Get today's stats stats = client.get_stats('2023-08-31') print(f"Steps: {stats['totalSteps']}") # Get heart rate data hr_data = client.get_heart_rates('2023-08-31') print(f"Resting HR: {hr_data['restingHeartRate']}") ### Additional Resources - Source Code: ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~263-~263: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~264-~264: Use an accepted time format
Context: ...Resources - Source Code: example.py - API Documentation: Comprehensive metho...

(QB_NEW_EN_OTHER_ERROR_IDS_4)


[grammar] ~265-~265: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~266-~266: Use correct spacing
Context: ...rld usage examples in tests/ directory ## 🙏 Acknowledgments Special thanks to al...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~268-~268: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~270-~270: Use correct spacing
Context: ...rs who have helped improve this project: - Community Contributors: Bug reports, f...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~272-~272: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~273-~273: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~274-~274: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~275-~275: Use correct spacing
Context: ...rs**: Improving examples and user guides This project thrives thanks to community...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~277-~277: Use correct spacing
Context: ...s to community involvement and feedback. ## 💖 Support This Project If you find thi...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~279-~279: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~281-~281: Use correct spacing
Context: ...s continued development and maintenance: ### 🌟 Ways to Support - **⭐ Star this repo...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~283-~283: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[typographical] ~285-~285: To join two clauses or set off examples, consider using an em dash.
Context: ...s to Support - ⭐ Star this repository - Help others discover the project - **💰 Fina...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~285-~285: There might be a mistake here.
Context: ...ory** - Help others discover the project - 💰 Financial Support - Contribute to d...

(QB_NEW_EN_OTHER)


[typographical] ~286-~286: To join two clauses or set off examples, consider using an em dash.
Context: ...ver the project - 💰 Financial Support - Contribute to development and hosting costs - **🐛...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~286-~286: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[typographical] ~287-~287: To join two clauses or set off examples, consider using an em dash.
Context: ...t and hosting costs - 🐛 Report Issues - Help improve stability and compatibility -...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~287-~287: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[typographical] ~288-~288: To join two clauses or set off examples, consider using an em dash.
Context: ...d compatibility - 📖 Spread the Word - Share with other developers ### 💳 Financial...

(QB_NEW_EN_DASH_RULE_EM)


[grammar] ~288-~288: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~290-~290: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~297-~297: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~298-~298: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~299-~299: Use correct spacing
Context: ...ion for hundreds of hours of development Every contribution, no matter the size, ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~301-~301: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

28-28: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


82-82: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


83-83: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


87-87: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


100-100: Multiple headings with the same content

(MD024, no-duplicate-heading)


105-105: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


114-114: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


128-128: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


140-140: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


153-153: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


158-158: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


175-175: Multiple headings with the same content

(MD024, no-duplicate-heading)


180-180: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


183-183: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


184-184: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


191-191: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


196-196: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


211-211: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


212-212: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


217-217: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


236-236: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


238-238: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


243-243: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


246-246: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


247-247: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


263-263: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


264-264: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


296-296: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

🔇 Additional comments (3)
.gitignore (2)

47-49: Good addition: ignore VS Code workspace folder.


1-3: Appropriate ignores for local data and PDM lockfile.

Ignoring "your_data/" and "pdm.lock" aligns with keeping the repo clean for a library project.

README.md (1)

3-26: Menu preview looks great.

Concise, scannable categories; nice UX touch with emojis.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (16)
README.md (6)

28-28: Fix heading level increment (MD001).

Change “### API Coverage Statistics” to H2 to avoid skipping a level.

-### API Coverage Statistics
+## API Coverage Statistics

86-92: Clarify OAuth example and avoid secret-scanner noise.

Use env vars and explicit placeholders.

-# Optional: Custom OAuth consumer (before login)
-import garth
-garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'}
+# Optional: Custom OAuth consumer (before login)
+import os, garth
+garth.sso.OAUTH_CONSUMER = {
+    'key': os.getenv('GARTH_OAUTH_KEY', '<YOUR_KEY>'),
+    'secret': os.getenv('GARTH_OAUTH_SECRET', '<YOUR_SECRET>')
+}

96-103: Remove duplicate “🧪 Testing” heading/content (MD024/MD022).

-## 🧪 Testing
-
-Run the test suite to verify functionality:
-
 ## 🧪 Testing
 
 Run the test suite to verify functionality:

236-247: Surround headings and fences with blank lines (MD022/MD031).

-### Interactive Demo
-Run the comprehensive API demonstration:
+### Interactive Demo
+
+Run the comprehensive API demonstration:
@@
-### Jupyter Notebook
-Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/reference.ipynb).
+### Jupyter Notebook
+
+Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/reference.ipynb).
@@
-### Python Code Examples
-```python
+### Python Code Examples
+
+```python

254-261: Avoid stale hard-coded dates in examples.

Compute today’s date programmatically.

-# Get today's stats
-stats = client.get_stats('2023-08-31')
+# Get today's stats
+from datetime import date
+today = date.today().strftime('%Y-%m-%d')
+stats = client.get_stats(today)
@@
-# Get heart rate data
-hr_data = client.get_heart_rates('2023-08-31')
+# Get heart rate data
+hr_data = client.get_heart_rates(today)

171-178: Remove duplicate “📦 Publishing” heading (MD024).

-## 📦 Publishing
-
-For package maintainers:
-
 ## 📦 Publishing
 
 For package maintainers:
pyproject.toml (3)

83-94: Enable Bandit rules (“S”) since S101 is referenced.

Per-file ignore uses S101 but “S” isn’t selected.

 [tool.ruff.lint]
 select = [
     "E",   # pycodestyle errors
     "W",   # pycodestyle warnings
     "F",   # pyflakes
     "I",   # isort
     "B",   # flake8-bugbear
     "C4",  # flake8-comprehensions
     "UP",  # pyupgrade
     "ARG", # flake8-unused-arguments
     "SIM", # flake8-simplify
+    "S",   # flake8-bandit
 ]
 
 [tool.ruff.lint.per-file-ignores]
 "tests/*" = ["ARG", "S101"]

Also applies to: 101-103


124-128: Run ruff --fix before isort/black to reduce churn.

-format = {composite = ["pdm run isort garminconnect tests", "pdm run black -l 88 garminconnect tests", "pdm run ruff check garminconnect tests --fix"]}
+format = {composite = ["pdm run ruff check garminconnect tests --fix", "pdm run isort garminconnect tests", "pdm run black -l 88 garminconnect tests"]}

76-78: Exclude non-runtime assets from distributions (optional).

Avoid shipping fixtures/CI files in wheels/sdists.

 [tool.pdm]
 distribution = true
+
+[tool.pdm.build]
+excludes = [
+  "test_data/**",
+  ".github/**",
+]
garminconnect/__init__.py (7)

1803-1810: Fix debug message (copy/paste).

-logger.debug("Requesting split summaries for activity id %s", activity_id)
+logger.debug("Requesting HR time-in-zones for activity id %s", activity_id)

1417-1423: Validate fordate with _validate_date_format().

Prevents 4xxs and standardizes behavior across date endpoints.

 def get_activities_fordate(self, fordate: str) -> dict[str, Any]:
     """Return available activities for date."""
-    url = f"{self.garmin_connect_activity_fordate}/{fordate}"
+    fordate = _validate_date_format(fordate, "fordate")
+    url = f"{self.garmin_connect_activity_fordate}/{fordate}"
     logger.debug(f"Requesting activities for date {fordate}")
     return self.connectapi(url)

270-287: Classify HTTP errors by status code; avoid brittle string checks.

 def connectapi(self, path: str, **kwargs: Any) -> Any:
     """Wrapper for garth connectapi with error handling."""
     try:
         return self.garth.connectapi(path, **kwargs)
     except Exception as e:
-        logger.error(f"API call failed for path '{path}': {e}")
-        # Re-raise with more context but preserve original exception type
-        if "auth" in str(e).lower() or "401" in str(e):
-            raise GarminConnectAuthenticationError(
-                f"Authentication failed: {e}"
-            ) from e
-        elif "429" in str(e) or "rate" in str(e).lower():
-            raise GarminConnectTooManyRequestsError(
-                f"Rate limit exceeded: {e}"
-            ) from e
-        else:
-            raise GarminConnectConnectionError(f"Connection error: {e}") from e
+        logger.exception("API call failed for path '%s'", path)
+        status = getattr(getattr(e, "response", None), "status_code", None)
+        if status == 401:
+            raise GarminConnectAuthenticationError(f"Authentication failed: {e}") from e
+        if status == 429:
+            raise GarminConnectTooManyRequestsError(f"Rate limit exceeded: {e}") from e
+        raise GarminConnectConnectionError(f"Connection error: {e}") from e

288-295: Align download() error handling with connectapi().

 def download(self, path: str, **kwargs: Any) -> Any:
     """Wrapper for garth download with error handling."""
     try:
         return self.garth.download(path, **kwargs)
     except Exception as e:
-        logger.error(f"Download failed for path '{path}': {e}")
-        raise GarminConnectConnectionError(f"Download error: {e}") from e
+        logger.exception("Download failed for path '%s'", path)
+        status = getattr(getattr(e, "response", None), "status_code", None)
+        if status == 401:
+            raise GarminConnectAuthenticationError(f"Download error: {e}") from e
+        if status == 429:
+            raise GarminConnectTooManyRequestsError(f"Download error: {e}") from e
+        raise GarminConnectConnectionError(f"Download error: {e}") from e

390-395: Raise ConnectionError for non-auth login failures and keep traceback.

-        else:
-            logger.error(f"Login failed: {e}")
-            raise GarminConnectAuthenticationError(f"Login failed: {e}") from e
+        else:
+            logger.exception("Login failed")
+            raise GarminConnectConnectionError(f"Login failed: {e}") from e

1927-1936: upload_workout: verify parsed payload type before posting.

 if isinstance(workout_json, str):
     import json as _json
     try:
         payload = _json.loads(workout_json)
     except Exception as e:
         raise ValueError(f"Invalid workout_json string: {e}") from e
 else:
     payload = workout_json
+if not isinstance(payload, (dict, list)):
+    raise ValueError("workout_json must be a JSON object or array")
 return self.garth.post("connectapi", url, json=payload, api=True).json()

1897-1903: Bug: get_workouts treats “end” as limit. Compute limit or rename param.

Current call returns too many/too few items depending on start.

 def get_workouts(self, start: int = 0, end: int = 100) -> dict[str, Any]:
     """Return workouts from start till end."""
     url = f"{self.garmin_workouts}/workouts"
-    logger.debug(f"Requesting workouts from {start}-{end}")
-    params = {"start": start, "limit": end}
+    if end < start:
+        raise ValueError("end must be >= start")
+    limit = end - start if end != 0 else 0
+    logger.debug(f"Requesting workouts from {start}-{end} (limit={limit})")
+    params = {"start": start, "limit": limit}
     return self.connectapi(url, params=params)

Alternative: deprecate “end” and expose “limit” explicitly.

🧹 Nitpick comments (5)
tests/conftest.py (2)

40-45: Harden Set-Cookie sanitization for string vs list cases.

Some VCR adapters store Set-Cookie as a string.

 for key in ["set-cookie", "Set-Cookie"]:
     if key in response["headers"]:
-        cookies = response["headers"][key]
-        sanitized_cookies = [sanitize_cookie(cookie) for cookie in cookies]
-        response["headers"][key] = sanitized_cookies
+        raw = response["headers"][key]
+        cookies = raw if isinstance(raw, list) else [raw]
+        response["headers"][key] = [sanitize_cookie(c) for c in cookies]

46-71: Guard body decoding for bytes/str.

Handle both bytes and str to avoid AttributeError.

-    body = response["body"]["string"].decode("utf8")
+    raw_body = response["body"]["string"]
+    body = raw_body.decode("utf8") if isinstance(raw_body, (bytes, bytearray)) else str(raw_body)
garminconnect/__init__.py (3)

541-556: Add date validation to get_body_composition.

Keep date handling consistent with other endpoints.

 def get_body_composition(
     self, startdate: str, enddate: str | None = None
 ) -> dict[str, Any]:
@@
-    if enddate is None:
-        enddate = startdate
+    if enddate is None:
+        enddate = startdate
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate, "enddate")
@@
-    params = {"startDate": str(startdate), "endDate": str(enddate)}
+    params = {"startDate": startdate, "endDate": enddate}

666-683: Validate dates in weight endpoints.

Prevents invalid requests and aligns with other methods.

 def get_weigh_ins(self, startdate: str, enddate: str) -> dict[str, Any]:
     """Get weigh-ins between startdate and enddate using format 'YYYY-MM-DD'."""
-    url = f"{self.garmin_connect_weight_url}/weight/range/{startdate}/{enddate}"
+    startdate = _validate_date_format(startdate, "startdate")
+    enddate = _validate_date_format(enddate, "enddate")
+    url = f"{self.garmin_connect_weight_url}/weight/range/{startdate}/{enddate}"
@@
 def get_daily_weigh_ins(self, cdate: str) -> dict[str, Any]:
     """Get weigh-ins for 'cdate' format 'YYYY-MM-DD'."""
-    url = f"{self.garmin_connect_weight_url}/weight/dayview/{cdate}"
+    cdate = _validate_date_format(cdate, "cdate")
+    url = f"{self.garmin_connect_weight_url}/weight/dayview/{cdate}"

1544-1551: Return JSON from upload_activity for consistency.

Other mutating endpoints return parsed JSON.

-                    return self.garth.post("connectapi", url, files=files, api=True)
+                    return self.garth.post("connectapi", url, files=files, api=True).json()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b1a7f56 and 372e5b7.

📒 Files selected for processing (5)
  • .coderabbit.yaml (1 hunks)
  • README.md (1 hunks)
  • garminconnect/__init__.py (60 hunks)
  • pyproject.toml (5 hunks)
  • tests/conftest.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~77-~77: There might be a mistake here.
Context: ... garminconnect ``` ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~81-~81: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~82-~82: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authe...

(QB_NEW_EN)


[grammar] ~83-~83: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~93-93: There might be a mistake here.
Context: ...: 'your_secret'} ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ... to verify functionality: ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~121-~121: There might be a mistake here.
Context: ... tokens for testing. ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~171-~171: There might be a mistake here.
Context: ...ode quality standards. ## 📦 Publishing For package maintainers: ## 📦 Publishi...

(QB_NEW_EN)


[grammar] ~175-~175: There might be a mistake here.
Context: ...r package maintainers: ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~201-~201: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~205-~205: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~206-~206: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~207-~207: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~210-~210: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~211-~211: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~234-~234: There might be a mistake here.
Context: ...in your-branch ``` ## 💻 Usage Examples ### Interactive Demo Run the comprehensive A...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ... 💻 Usage Examples ### Interactive Demo Run the comprehensive API demonstration:...

(QB_NEW_EN)


[grammar] ~243-~243: There might be a mistake here.
Context: ...s ./example.py ``` ### Jupyter Notebook Explore the API interactively with our [...

(QB_NEW_EN)


[grammar] ~263-~263: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~265-~265: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~268-~268: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~272-~272: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~273-~273: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~274-~274: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~279-~279: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~283-~283: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~286-~286: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~287-~287: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~288-~288: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~290-~290: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~297-~297: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~298-~298: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~301-~301: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

28-28: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


100-100: Multiple headings with the same content

(MD024, no-duplicate-heading)


175-175: Multiple headings with the same content

(MD024, no-duplicate-heading)

🔇 Additional comments (1)
.coderabbit.yaml (1)

26-31: Path instructions look good.

Guidance for garminconnect/** is clear and appropriate.

reviews:
profile: "assertive"
request_changes_workflow: false
profile: "pythonic"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid reviews.profile value (must be 'chill' or 'assertive').

"pythonic" is not allowed by the schema and breaks parsing.

-  profile: "pythonic"
+  profile: "chill"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
profile: "pythonic"
profile: "chill"
🤖 Prompt for AI Agents
In .coderabbit.yaml around line 5, the reviews.profile value "pythonic" is
invalid per schema and breaks parsing; replace it with one of the allowed values
("chill" or "assertive")—e.g., set profile: "chill"—and ensure the file
validates against the schema after the change.

Comment on lines +14 to +18
auto_fix:
enabled: true
include_imports: true
include_type_hints: true
include_security_fixes: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unsupported reviews.auto_fix block (schema mismatch).

reviews.auto_fix is not defined in the published schema; it will fail validation. Drop it or relocate per product docs.

-  auto_fix:
-    enabled: true
-    include_imports: true
-    include_type_hints: true
-    include_security_fixes: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto_fix:
enabled: true
include_imports: true
include_type_hints: true
include_security_fixes: true
🤖 Prompt for AI Agents
In .coderabbit.yaml around lines 14 to 18, the reviews.auto_fix block is invalid
per the published schema and will cause validation failures; remove the
unsupported auto_fix section (or relocate its keys to the correct supported
configuration per product docs) so the file only contains schema-supported
fields; ensure you update any references to these options elsewhere and run
schema validation or linting to confirm the YAML passes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
.github/dependabot.yml (2)

7-9: Daily schedule with explicit time/timezone looks good; note it runs Mon–Fri only.

Apply one of these diffs:

Option A — include weekends via cron:

   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
-      interval: daily
-      time: "20:00"
-      timezone: "Europe/Amsterdam"
+      interval: cron
+      cronjob: "0 20 * * *"
+      timezone: "Europe/Amsterdam"

Option B — keep daily but group PRs:

   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
       interval: daily
       time: "20:00"
       timezone: "Europe/Amsterdam"
+    groups:
+      gha-minor-patch:
+        update-types:
+          - "minor"
+          - "patch"

14-16: pip schedule is fine; consider noise-reduction knobs.

  • Add increase-if-necessary to cut unnecessary bumps; supported for Python. (docs.github.com, github.blog)
  • Optional: group minor/patch updates; use cron if weekend coverage is desired. (github.blog)
   - package-ecosystem: "pip"
     directory: "/"
     schedule:
       interval: daily
       time: "20:00"
       timezone: "Europe/Amsterdam"
+    versioning-strategy: increase-if-necessary
+    groups:
+      pip-minor-patch:
+        update-types:
+          - "minor"
+          - "patch"

If you want weekends too:

     schedule:
-      interval: daily
-      time: "20:00"
-      timezone: "Europe/Amsterdam"
+      interval: cron
+      cronjob: "0 20 * * *"
+      timezone: "Europe/Amsterdam"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 372e5b7 and c51841e.

📒 Files selected for processing (1)
  • .github/dependabot.yml (1 hunks)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
garminconnect/__init__.py (1)

136-138: Fix missing leading slashes in endpoint URL constants

Update the following in garminconnect/init.py to ensure requests hit the correct paths:

--- a/garminconnect/__init__.py
+++ b/garminconnect/__init__.py
@@ 136,138c136,138
-        self.garmin_connect_set_hydration_url = (
-            "usersummary-service/usersummary/hydration/log"
-        )
+        self.garmin_connect_set_hydration_url = (
+            "/usersummary-service/usersummary/hydration/log"
+        )
@@ 194,196c194,196
-        self.garmin_connect_pregnancy_snapshot_url = (
-            "periodichealth-service/menstrualcycle/pregnancysnapshot"
-        )
+        self.garmin_connect_pregnancy_snapshot_url = (
+            "/periodichealth-service/menstrualcycle/pregnancysnapshot"
+        )
@@ 257,259c257,259
-        self.garmin_graphql_endpoint = "graphql-gateway/graphql"
+        self.garmin_graphql_endpoint = "/graphql-gateway/graphql"
♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

1958-1967: Validate parsed workout payload type (must be object or array).

If the JSON string parses to a scalar (e.g., "42"), you’ll post invalid JSON.

Apply:

         else:
             payload = workout_json
+        if not isinstance(payload, (dict, list)):
+            raise ValueError("workout_json must be a JSON object or array")
         return self.garth.post("connectapi", url, json=payload, api=True).json()
🧹 Nitpick comments (4)
garminconnect/__init__.py (4)

2001-2006: Replace “{TBD}” in GraphQL docstring with concrete guidance.

Docstrings with placeholders confuse users.

Apply:

-        """Returns the results of a POST request to the Garmin GraphQL Endpoints.
-        Requires a GraphQL structured query.  See {TBD} for examples.
-        """
+        """Execute a POST to Garmin's GraphQL endpoint.
+
+        Args:
+            query: A GraphQL request body, e.g. {"query": "...", "variables": {...}}
+
+        Returns:
+            Parsed JSON response as a dict.
+        """

1066-1073: Tighten return type hints to list[dict[str, Any]].

Current hints use list[dict], losing key/value typing.

Apply:

-def get_available_badges(self) -> list[dict]:
+def get_available_badges(self) -> list[dict[str, Any]]:
@@
-def get_in_progress_badges(self) -> list[dict]:
+def get_in_progress_badges(self) -> list[dict[str, Any]]:

Also applies to: 1074-1105


636-638: Unify error message casing for unitKey.

Elsewhere you use “unitKey”. Keep it consistent for UX.

Apply:

-            raise ValueError(f"UnitKey must be one of {VALID_WEIGHT_UNITS}")
+            raise ValueError(f"unitKey must be one of {VALID_WEIGHT_UNITS}")

752-781: Optional: validate blood pressure inputs.

Add simple range/type checks for systolic/diastolic/pulse to prevent bad writes.

I can draft a small validator mirroring your integer validators if you want it added.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7b8bd08 and fcc493b.

📒 Files selected for processing (1)
  • garminconnect/__init__.py (54 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
🔇 Additional comments (1)
garminconnect/__init__.py (1)

270-291: HTTP error mapping looks solid.

Consistent handling of 401/429 and raising on all other HTTP errors. Nice.

Also applies to: 292-305

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
garminconnect/__init__.py (3)

136-138: Missing leading slash in set-hydration endpoint path.

Without “/”, requests may hit an invalid URL.

-        self.garmin_connect_set_hydration_url = (
-            "usersummary-service/usersummary/hydration/log"
-        )
+        self.garmin_connect_set_hydration_url = (
+            "/usersummary-service/usersummary/hydration/log"
+        )

194-196: Missing leading slash in pregnancy snapshot endpoint.

Align with other endpoints to avoid path-join issues.

-        self.garmin_connect_pregnancy_snapshot_url = (
-            "periodichealth-service/menstrualcycle/pregnancysnapshot"
-        )
+        self.garmin_connect_pregnancy_snapshot_url = (
+            "/periodichealth-service/menstrualcycle/pregnancysnapshot"
+        )

1885-1896: Return type mismatch: get_gear_activities likely returns a list.

Endpoint returns activities; update annotation to list[dict].

-    def get_gear_activities(self, gearUUID: str, limit: int = 9999) -> dict[str, Any]:
+    def get_gear_activities(self, gearUUID: str, limit: int = 9999) -> list[dict[str, Any]]:
@@
-        return self.connectapi(url)
+        return self.connectapi(url)
♻️ Duplicate comments (6)
README.md (5)

168-173: Avoid inlined secrets; use env vars and clear placeholders.

Prevents secret-scanner noise and clarifies configuration.

-```python
-# Optional: Custom OAuth consumer (before login)
-import garth
-garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'}
-```
+```python
+# Optional: Custom OAuth consumer (before login)
+import os
+import garth
+garth.sso.OAUTH_CONSUMER = {
+    'key': os.getenv('GARTH_OAUTH_KEY', '<YOUR_KEY>'),
+    'secret': os.getenv('GARTH_OAUTH_SECRET', '<YOUR_SECRET>'),
+}
+# Note: Set these env vars securely; placeholders are non-sensitive.
+```

266-272: Surround headings with blank lines (MD022/MD031).

Add a blank line after “Jupyter Notebook” and “Python Code Examples”.

-### Jupyter Notebook
-Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/reference.ipynb).
+### Jupyter Notebook
+
+Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/reference.ipynb).
@@
-### Python Code Examples
-```python
+### Python Code Examples
+
+```python

277-284: Avoid stale, hard-coded dates in examples.

Compute today once and reuse.

-# Get today's stats
-stats = client.get_stats('2023-08-31')
+# Get today's stats
+from datetime import date
+_today = date.today().strftime('%Y-%m-%d')
+stats = client.get_stats(_today)
@@
-# Get heart rate data
-hr_data = client.get_heart_rates('2023-08-31')
+# Get heart rate data
+hr_data = client.get_heart_rates(_today)

204-206: Remove duplicate “📦 Publishing” heading (MD024).

Second heading repeats the one at Line 200.

-## 📦 Publishing
-
-For package maintainers:

28-31: Fix heading level to satisfy MD001 (heading-increment).

Change "### API Coverage Statistics" to H2.

-### API Coverage Statistics
+## API Coverage Statistics
garminconnect/__init__.py (1)

339-354: Fix early return in non-MFA login; profile/settings never load.

This regresses display_name/unit_system population and contradicts docs.

                 else:
                     token1, token2 = self.garth.login(
                         self.username,
                         self.password,
                         prompt_mfa=self.prompt_mfa,
                     )
-                    # In MFA early-return mode, profile/settings are not loaded yet
-                    return token1, token2
+                    # Continue to load profile/settings below
🧹 Nitpick comments (6)
.pre-commit-config.yaml (3)

28-34: Pick one formatter: Black or ruff-format, not both.

Running both increases time and risks churn on edge cases. Prefer a single source of truth.

Option A (keep Black, drop ruff-format):

-  - id: ruff
-    args: [--fix]
-  - id: ruff-format
+  - id: ruff
+    args: [--fix]

Option B (use ruff-format only, drop Black block above):

- - repo: https://github.com/psf/black
-   rev: 24.8.0
-   hooks:
-   - id: black
-     language_version: python3

45-45: Same portability concern for lint hook; use PATHed pdm.

Keep the hook OS-agnostic; also “-s” avoids spinners/prompts in CI logs.

-    entry: .venv/bin/pdm run lint
+    entry: pdm run -s lint

21-27: Drop unnecessary Black extras
Remove the ‘.[jupyter]’ extra—not needed when notebooks are excluded:

-  - id: black
-    additional_dependencies: ['.[jupyter]']
-    language_version: python3
+  - id: black
+    language_version: python3
README.md (1)

79-85: Label the fenced block with a language (MD040).

Add bash to the “Run demo software” block.

-```
+```bash
 python3 -m venv .venv --copies
 source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 pip install pdm
 pdm install --group :example
 ./example.py

</blockquote></details>
<details>
<summary>garminconnect/__init__.py (2)</summary><blockquote>

`229-230`: **Duplicate daily-stress constants; keep one source of truth.**

Both garmin_all_day_stress_url and garmin_connect_daily_stress_url point to the same path. Prefer one and reuse.

Do you want me to submit a small patch deduplicating and replacing one reference?


Also applies to: 165-166

---

`2001-2004`: **Docstring placeholder "{TBD}"—replace with a concrete reference.**

Link to an example or remove the placeholder.

```diff
-        """Returns the results of a POST request to the Garmin GraphQL Endpoints.
-        Requires a GraphQL structured query.  See {TBD} for examples.
-        """
+        """POST a GraphQL query to Garmin’s GraphQL endpoint.
+        Expects a JSON-serializable GraphQL payload (e.g., {"query": "...", "variables": {...}})."""
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fcc493b and 162a085.

📒 Files selected for processing (6)
  • .gitignore (3 hunks)
  • .pre-commit-config.yaml (1 hunks)
  • .vscode/settings.json (1 hunks)
  • README.md (1 hunks)
  • garminconnect/__init__.py (54 hunks)
  • pyproject.toml (6 hunks)
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • .vscode/settings.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...le ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~158-~158: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~162-~162: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~163-~163: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~164-~164: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~174-174: There might be a mistake here.
Context: ...: 'your_secret'} ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~177-~177: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~200-~200: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: ## 📦 Publishi...

(QB_NEW_EN)


[grammar] ~204-~204: There might be a mistake here.
Context: ...r package maintainers: ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~230-~230: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~234-~234: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~235-~235: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~239-~239: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~240-~240: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~266-~266: There might be a mistake here.
Context: ...in your-branch ``` ### Jupyter Notebook Explore the API interactively with our [...

(QB_NEW_EN)


[grammar] ~286-~286: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~288-~288: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~291-~291: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~295-~295: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~297-~297: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~302-~302: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~306-~306: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~309-~309: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~310-~310: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~311-~311: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~313-~313: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~319-~319: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~320-~320: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~321-~321: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

28-28: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


79-79: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


204-204: Multiple headings with the same content

(MD024, no-duplicate-heading)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
🔇 Additional comments (1)
.pre-commit-config.yaml (1)

37-42: Use PATH-installed pdm instead of a hardcoded .venv path

CI workflows install pdm via pip and never reference .venv; switching to pdm run -s format is cross-platform and respects your [tool.pdm.scripts].format definition.

-    entry: .venv/bin/pdm run format
+    entry: pdm run -s format

Optional: remove this hook entirely and rely on your existing Black/Ruff hooks to avoid duplication.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
garminconnect/__init__.py (1)

2016-2020: Sanitize GraphQL debug logging (avoid leaking variables/PII)

Logging the full query dumps variables into logs. Log operation name and variable keys instead.

-        logger.debug(f"Querying Garmin GraphQL Endpoint with query: {query}")
+        op = (query.get("operationName") or "unnamed") if isinstance(query, dict) else "unnamed"
+        vars_keys = sorted((query.get("variables") or {}).keys()) if isinstance(query, dict) else []
+        logger.debug("Querying Garmin GraphQL op=%s vars=%s", op, vars_keys)
♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

1360-1379: Return type mismatch for get_device_solar_data

Function returns resp["deviceSolarInput"] (a list) but is annotated as dict. Fix the annotation to match behavior.

-    def get_device_solar_data(
-        self, device_id: str, startdate: str, enddate: str | None = None
-    ) -> dict[str, Any]:
+    def get_device_solar_data(
+        self, device_id: str, startdate: str, enddate: str | None = None
+    ) -> list[dict[str, Any]]:
🧹 Nitpick comments (7)
README.md (2)

28-43: Add a datestamp to stabilize “API Coverage” counts

These numbers can drift as the API evolves. Add an “as of” date so readers know when the snapshot was taken.

-## API Coverage Statistics
+## API Coverage Statistics (as of 2025-08-31)
@@
-- **Total API Methods**: 101 unique endpoints
+- **Total API Methods**: 101 unique endpoints (snapshot)

98-114: Avoid duplicating tool installs across setup sections

“pip install pdm” and “black/codespell” show up in multiple places. Consolidate to one clear path to reduce confusion.

Example: keep installs only under “Environment Setup” and, in “Run demo software,” assume PDM is already installed (or link back).

garminconnect/__init__.py (5)

895-915: get_lactate_threshold: normalize/validate dates for range queries

start_date/end_date can be date or str but are used directly in URLs. Normalize to strings and validate when strings are provided.

-        if start_date is None:
+        if start_date is None:
             raise ValueError("you must either specify 'latest=True' or a start_date")
@@
-        if end_date is None:
-            end_date = date.today().isoformat()
+        if end_date is None:
+            end_date = date.today()
+
+        # Normalize and validate
+        if isinstance(start_date, date):
+            start_date = start_date.isoformat()
+        else:
+            start_date = _validate_date_format(start_date, "start_date")
+        if isinstance(end_date, date):
+            end_date = end_date.isoformat()
+        else:
+            end_date = _validate_date_format(end_date, "end_date")

1428-1428: Prefer parameterized logging over f-strings in logger calls

Avoid eager string formatting when the log level is disabled.

-        logger.debug(f"Requesting activities from {start} with limit {limit}")
+        logger.debug("Requesting activities from %d with limit %d", start, limit)
@@
-        logger.debug(f"Changing activity type: {str(payload)}")
+        logger.debug("Changing activity type: %s", payload)
@@
-        logger.debug(f"Requesting fitnessstats by date from {startdate} to {enddate}")
+        logger.debug("Requesting fitnessstats by date from %s to %s", startdate, enddate)
@@
-        logger.debug(f"Requesting workouts from {start} with limit {limit}")
+        logger.debug("Requesting workouts from %d with limit %d", start, limit)
@@
-        logger.debug(f"Requesting menstrual data for dates {startdate} through {enddate}")
+        logger.debug("Requesting menstrual data for dates %s through %s", startdate, enddate)

Also applies to: 1471-1471, 1678-1679, 1934-1934, 1992-1994


687-698: Validate cdate in delete_weigh_in for direct callers

delete_weigh_ins validates via get_daily_weigh_ins, but delete_weigh_in can be called directly. Add cdate validation here too.

-    def delete_weigh_in(self, weight_pk: str, cdate: str) -> Any:
+    def delete_weigh_in(self, weight_pk: str, cdate: str) -> Any:
+        cdate = _validate_date_format(cdate, "cdate")

1886-1899: get_gear_activities: validate limit and guard against huge values

Validate limit as positive int and optionally cap it to a sane maximum to avoid server strain.

-    def get_gear_activities(
-        self, gearUUID: str, limit: int = 9999
-    ) -> list[dict[str, Any]]:
+    def get_gear_activities(
+        self, gearUUID: str, limit: int = 9999
+    ) -> list[dict[str, Any]]:
+        limit = _validate_positive_integer(limit, "limit")
@@
-        url = f"{self.garmin_connect_activities_baseurl}{gearUUID}/gear?start=0&limit={limit}"
+        url = f"{self.garmin_connect_activities_baseurl}{gearUUID}/gear?start=0&limit={limit}"

615-626: Unify timestamp formatting across weigh-in and hydration endpoints

Weigh-ins use truncated ISO with “.00”; hydration uses %f microseconds. Consider a single helper to avoid drift.

Example helper (apply wherever timestamps are built):

def _fmt_ts(dt: datetime) -> str:
    # Use ms precision to match server expectations
    return dt.replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]

Then:

  • dateTimestamp/gmtTimestamp: f"{_fmt_ts(dt)}.000" (or align both to the same convention)
  • hydration timestampLocal: _fmt_ts(raw_ts)

Also applies to: 639-665, 929-994

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 162a085 and 23d3c12.

📒 Files selected for processing (3)
  • .pre-commit-config.yaml (1 hunks)
  • README.md (1 hunks)
  • garminconnect/__init__.py (55 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .pre-commit-config.yaml
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...le ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~158-~158: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~162-~162: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~163-~163: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~164-~164: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~179-179: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~182-~182: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~205-~205: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~231-~231: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~235-~235: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~237-~237: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~240-~240: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~241-~241: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~290-~290: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~292-~292: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~295-~295: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~299-~299: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~300-~300: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~301-~301: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~306-~306: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~310-~310: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~313-~313: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~314-~314: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~315-~315: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~317-~317: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~323-~323: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~325-~325: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~328-~328: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
🔇 Additional comments (1)
garminconnect/__init__.py (1)

320-356: Confirm tokenstore path populates profile/display_name

After load/loads() you require self.garth.profile to exist; some token stores may not preload profile, causing “Failed to retrieve profile”. Validate this with your token flow and consider fetching profile when missing before raising.

If profile isn’t auto-populated, fetch it once after token load and set display_name/full_name, then continue to settings.

Comment on lines +1971 to +1973
if not isinstance(payload, dict | list):
raise ValueError("workout_json must be a JSON object or array")
return self.garth.post("connectapi", url, json=payload, api=True).json()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Runtime bug: isinstance(..., dict | list) raises TypeError

isinstance does not accept union (|) at runtime. Use a tuple.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not isinstance(payload, dict | list):
raise ValueError("workout_json must be a JSON object or array")
return self.garth.post("connectapi", url, json=payload, api=True).json()
if not isinstance(payload, (dict, list)):
raise ValueError("workout_json must be a JSON object or array")
return self.garth.post("connectapi", url, json=payload, api=True).json()
🤖 Prompt for AI Agents
In garminconnect/__init__.py around lines 1971 to 1973, the runtime use of
isinstance(payload, dict | list) raises a TypeError because isinstance does not
accept the union operator; replace it with a tuple form like isinstance(payload,
(dict, list)) so the type check works at runtime and retains the same semantics.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
garminconnect/__init__.py (1)

917-994: Hydration timestamp parsing is too strict; accept ISO without microseconds.

The strptime("%Y-%m-%dT%H:%M:%S.%f") pattern forces microseconds. Using fromisoformat tolerates both with/without fractional seconds and is consistent with add_weigh_in.

-            try:
-                raw_ts = datetime.strptime(cdate, "%Y-%m-%d")
-                timestamp = datetime.strftime(raw_ts, "%Y-%m-%dT%H:%M:%S.%f")
-            except ValueError as e:
-                raise ValueError(f"invalid cdate: {e}") from e
+            try:
+                raw_ts = datetime.fromisoformat(cdate)
+            except ValueError:
+                # if cdate is just a date, parse with format and set time to 00:00:00
+                raw_ts = datetime.strptime(cdate, DATE_FORMAT_STR)
+            timestamp = raw_ts.replace(
+                hour=0, minute=0, second=0, microsecond=0
+            ).isoformat(timespec="microseconds")
@@
-            try:
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+            try:
+                raw_ts = datetime.fromisoformat(timestamp)
             except ValueError as e:
                 raise ValueError(
                     f"Invalid timestamp format (expected YYYY-MM-DDTHH:MM:SS.ffffff): {e}"
                 ) from e
@@
-            try:
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+            try:
+                raw_ts = datetime.fromisoformat(timestamp)
♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

1954-1974: Runtime bug: isinstance(..., dict | list) raises TypeError.

This union in isinstance is invalid at runtime; use a tuple. Also, keep the earlier payload-type validation.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")
🧹 Nitpick comments (5)
garminconnect/__init__.py (5)

269-305: HTTP error mapping is clear; consider logging tracebacks for unexpected exceptions.

connectapi/download wrappers consistently classify 401/429 vs other HTTP errors. Minor: for unexpected Exception branches, use logger.exception to retain stack traces.

-        except Exception as e:
-            raise GarminConnectConnectionError(f"Connection error: {e}") from e
+        except Exception as e:
+            logger.exception("Connection error during connectapi path=%s", path)
+            raise GarminConnectConnectionError(f"Connection error: {e}") from e
-        except Exception as e:
+        except Exception as e:
+            logger.exception("Download error path=%s", path)
             status = getattr(getattr(e, "response", None), "status_code", None)

473-486: Use DATE_FORMAT_STR constant for parsing to avoid format drift.

Small consistency nit: reuse the module constant rather than retyping the format.

-        start_date = datetime.strptime(start, "%Y-%m-%d").date()
-        end_date = datetime.strptime(end, "%Y-%m-%d").date()
+        start_date = datetime.strptime(start, DATE_FORMAT_STR).date()
+        end_date = datetime.strptime(end, DATE_FORMAT_STR).date()

552-596: add_body_composition: guard timestamp parsing.

datetime.fromisoformat will raise ValueError on bad input; mirror the defensive handling you used elsewhere (e.g., add_weigh_in).

-        dt = datetime.fromisoformat(timestamp) if timestamp else datetime.now()
+        if timestamp:
+            try:
+                dt = datetime.fromisoformat(timestamp)
+            except ValueError as e:
+                raise ValueError(f"invalid timestamp format: {e}") from e
+        else:
+            dt = datetime.now()

762-784: Consider lightweight range checks for blood pressure values.

Optional guardrails (e.g., systolic 70–260, diastolic 40–150, pulse 20–250) would prevent obvious bad writes.

         payload = {
@@
         }
+        for name, val, lo, hi in (
+            ("systolic", systolic, 70, 260),
+            ("diastolic", diastolic, 40, 150),
+            ("pulse", pulse, 20, 250),
+        ):
+            if not isinstance(val, int) or not (lo <= val <= hi):
+                raise ValueError(f"{name} must be an int in [{lo}, {hi}]")

1885-1899: get_gear_activities: validate limit and pass as int.

Minor enhancement: enforce positive integer for limit like other paginated endpoints.

-    def get_gear_activities(
-        self, gearUUID: str, limit: int = 9999
-    ) -> list[dict[str, Any]]:
+    def get_gear_activities(
+        self, gearUUID: str, limit: int = 9999
+    ) -> list[dict[str, Any]]:
@@
-        url = f"{self.garmin_connect_activities_baseurl}{gearUUID}/gear?start=0&limit={limit}"
+        limit = _validate_positive_integer(limit, "limit")
+        url = f"{self.garmin_connect_activities_baseurl}{gearUUID}/gear?start=0&limit={limit}"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 23d3c12 and 9d28d87.

📒 Files selected for processing (1)
  • garminconnect/__init__.py (54 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
🔇 Additional comments (17)
garminconnect/__init__.py (17)

20-82: Validation utilities look solid and consistent.

Good centralization of regex/date constants and numeric validators; using numbers.Real avoids the isinstance union pitfall.


306-384: Login flow consistency: early-return MFA and profile/settings load look correct.

Consistent (token1, token2) return shape, short-circuit on return_on_mfa, and profile/settings fetch on normal credential path are all aligned with prior feedback.


419-437: User summary: solid input validation and explicit empty/priv-protected checks.

This mirrors the standardized pattern—nice.


597-627: Weigh-in: validation/readability LGTM.

Positive number validation and unit whitelist are clear; timestamp parsing has proper error messages.


723-754: Body Battery endpoints: input checks and return shapes look good.

Single-day vs range handling and list return types are appropriate.


1060-1107: In-progress badges computation reads well.

Nice reuse of a predicate and merging earned/available; guards for None/zero progress are clear.


1286-1303: Training status/fitness age: consistent date validation.

Both endpoints now validate cdate before building URLs.


1333-1340: Devices retrieval: wrapper usage LGTM.

Straight-through connectapi with debug logging.


1359-1379: Device solar data: return type and error handling are correct.

Type annotation matches the actual returned subfield; explicit key check is good.


1416-1436: Activities paging: robust fallback to [] when None.

Validated start/limit, max cap, and tolerant handling of dict/list API shapes downstream.


1514-1527: get_last_activity handles both list and dict shapes.

Defensive checks avoid crashes on unexpected responses.


1529-1581: upload_activity: good input validation and fixed with-block.

Extension whitelist via Enum is clear; context-managed file IO avoids leaks.


1601-1649: get_activities_by_date: paginated fetch loop is clear.

Validates dates, streams pages of 20; logs ranges—nice.


1686-1723: Goals pagination: input validators + loop are consistent.

Start/limit validated; logging shows index window.


1984-1997: Menstrual calendar/dayview: date validation looks consistent.

Inputs validated and URLs constructed deterministically.


2006-2030: GraphQL helper: useful logging of op/vars; returns parsed JSON.

Good addition; keeps surface minimal and typed.


2031-2036: Deprecation message uses warning level.

Appropriate log level for deprecation; message is clear.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

1989-1992: Runtime TypeError: isinstance(payload, dict | list) is invalid.

Use a tuple in isinstance. This was flagged earlier and still persists.

Apply:

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")
🧹 Nitpick comments (6)
README.md (3)

28-43: Avoid stale coverage snapshot; add provenance note or script link.

The fixed counts/date will drift. Add a one-liner that this is a snapshot and link to the script/command to regenerate it.


103-111: Remove duplicate installs of formatter/spell tools.

You install Black/codespell here and again below. Keep them only in the “optional tools” step.

Apply:

-# 2. Install PDM (Python Dependency Manager)
-pip install pdm "black[jupyter]" codespell
+# 2. Install PDM (Python Dependency Manager)
+pip install pdm

273-288: Use env vars in example to avoid hard-coded credentials and scanner noise.

Keeps samples safe and copy-pasteable.

Apply:

 from garminconnect import Garmin
+import os
 
-# Initialize and login
-client = Garmin('your_email', 'your_password')
+# Initialize and login
+client = Garmin(
+    os.getenv("GARMIN_EMAIL", "<YOUR_EMAIL>"),
+    os.getenv("GARMIN_PASSWORD", "<YOUR_PASSWORD>")
+)
 client.login()
garminconnect/__init__.py (3)

854-860: Harden handling when “power” isn’t a non-empty list.

Avoid IndexError/TypeError if API returns dict/empty list.

Apply:

-            power = self.connectapi(power_url)
-            try:
-                power_dict = power[0]
-            except IndexError:
-                # If no power available
-                power_dict = {}
+            power = self.connectapi(power_url)
+            if isinstance(power, list) and power:
+                power_dict = power[0]
+            elif isinstance(power, dict):
+                power_dict = power
+            else:
+                power_dict = {}

1123-1128: Validate pagination params consistently for challenge endpoints.

Align with get_activities/get_goals to prevent negatives/zero.

Apply:

 def get_adhoc_challenges(self, start: int, limit: int) -> dict[str, Any]:
     """Return adhoc challenges for current user."""
-    url = self.garmin_connect_adhoc_challenges_url
+    start = _validate_non_negative_integer(start, "start")
+    limit = _validate_positive_integer(limit, "limit")
+    url = self.garmin_connect_adhoc_challenges_url
@@
 def get_badge_challenges(self, start: int, limit: int) -> dict[str, Any]:
     """Return badge challenges for current user."""
-    url = self.garmin_connect_badge_challenges_url
+    start = _validate_non_negative_integer(start, "start")
+    limit = _validate_positive_integer(limit, "limit")
+    url = self.garmin_connect_badge_challenges_url
@@
 def get_available_badge_challenges(self, start: int, limit: int) -> dict[str, Any]:
     """Return available badge challenges."""
-    url = self.garmin_connect_available_badge_challenges_url
+    start = _validate_non_negative_integer(start, "start")
+    limit = _validate_positive_integer(limit, "limit")
+    url = self.garmin_connect_available_badge_challenges_url
@@
 def get_non_completed_badge_challenges(self, start: int, limit: int) -> dict[str, Any]:
     """Return badge non-completed challenges for current user."""
-    url = self.garmin_connect_non_completed_badge_challenges_url
+    start = _validate_non_negative_integer(start, "start")
+    limit = _validate_positive_integer(limit, "limit")
+    url = self.garmin_connect_non_completed_badge_challenges_url
@@
 def get_inprogress_virtual_challenges(self, start: int, limit: int) -> dict[str, Any]):
     """Return in-progress virtual challenges for current user."""
-    url = self.garmin_connect_inprogress_virtual_challenges_url
+    start = _validate_non_negative_integer(start, "start")
+    limit = _validate_positive_integer(limit, "limit")
+    url = self.garmin_connect_inprogress_virtual_challenges_url

Also applies to: 1132-1138, 1141-1147, 1150-1159, 1161-1170


1491-1492: Prefer lazy logger formatting over f-strings.

Avoid stringification when debug logging is disabled.

Apply:

-        logger.debug(f"Uploading manual activity: {str(payload)}")
+        logger.debug("Uploading manual activity: %s", payload)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9d28d87 and 9553b9f.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • garminconnect/__init__.py (51 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...le ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~158-~158: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~162-~162: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~163-~163: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~164-~164: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~179-179: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~182-~182: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~205-~205: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~231-~231: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~235-~235: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~237-~237: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~240-~240: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~241-~241: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~290-~290: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~292-~292: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~295-~295: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~299-~299: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~300-~300: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~301-~301: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~306-~306: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~310-~310: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~313-~313: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~314-~314: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~315-~315: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~317-~317: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~323-~323: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~325-~325: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~328-~328: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
garminconnect/__init__.py (1)

1-2094: Refactor timestamp formatting
No unsupported isinstance(x, A|B) usages found (PEP 604 allows union types in isinstance since Python 3.10) (peps.python.org, docs.python.org).
Mandatory: have _fmt_ts produce the full timestamp with trailing .000, and remove the explicit ".000" concatenations in add_weigh_in, add_weigh_in_with_timestamps, and set_blood_pressure so each payload uses dateTimestamp: _fmt_ts(dt) (and similar) directly.

♻️ Duplicate comments (2)
garminconnect/__init__.py (2)

2012-2014: Fix runtime TypeError: isinstance(..., dict | list) is invalid

At runtime, isinstance does not accept PEP 604 unions. This will raise TypeError when hit. Use a tuple.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")

625-627: Invalid timestamps: double “.000” appended to millisecond-formatted strings

_fmt_ts already emits milliseconds. Appending “.000” produces invalid values like 2025-09-04T12:34:56.123.000.

-            "dateTimestamp": f"{_fmt_ts(dt)}.000",
-            "gmtTimestamp": f"{_fmt_ts(dtGMT)}.000",
+            "dateTimestamp": _fmt_ts(dt),
+            "gmtTimestamp": _fmt_ts(dtGMT),
-            "dateTimestamp": f"{_fmt_ts(dt)}.000",  # Local time
-            "gmtTimestamp": f"{_fmt_ts(dtGMT)}.000",  # GMT/UTC time
+            "dateTimestamp": _fmt_ts(dt),   # Local time (ms)
+            "gmtTimestamp": _fmt_ts(dtGMT), # GMT/UTC time (ms)
-            "measurementTimestampLocal": f"{_fmt_ts(dt)}.000",
-            "measurementTimestampGMT": f"{_fmt_ts(dtGMT)}.000",
+            "measurementTimestampLocal": _fmt_ts(dt),
+            "measurementTimestampGMT": _fmt_ts(dtGMT),

Also applies to: 660-661, 780-781

🧹 Nitpick comments (13)
README.md (9)

3-26: Clarify that the menu output is illustrative and may vary by version.

Add a one-liner before the sample to set expectations and avoid future “stale output” reports.

-The Garmin Connect API demo (`example.py`) provides comprehensive access to **101 API methods** organized into **11 categories** for easy navigation:
+The Garmin Connect API demo (`example.py`) provides comprehensive access to **101 API methods** organized into **11 categories** for easy navigation:
+
+Note: The demo menu is generated dynamically; exact options may change between releases.

28-43: Prevent drift in “API Coverage Statistics”.

These numbers will age. Consider generating this section from a script (e.g., a PDM task) and/or linking to a source-of-truth report to reduce maintenance. At minimum, note that counts are auto-generated during releases.


46-51: Tighten phrasing in bullet points.

Minor grammar tweaks for smoother reading.

-- **Comprehensive Coverage**: All major Garmin Connect features accessible
-- **Error Handling**: Robust error handling and user-friendly prompts
+- **Comprehensive Coverage**: All major Garmin Connect features are accessible
+- **Error Handling**: Robust error handling with user-friendly prompts

69-75: Prefer python -m pip over bare pip for reliability.

Prevents PATH/env mismatches across platforms.

-```bash
-pip3 install garminconnect
-```
+```bash
+python3 -m pip install --upgrade pip
+python3 -m pip install garminconnect
+```

77-85: Make the demo invocation cross-platform.

Using python avoids execute-bit/shebang issues on Windows.

-./example.py
+python ./example.py

179-181: Add a note about securing the token directory.

Recommend restrictive permissions to protect OAuth tokens.

 Tokens are automatically saved to `~/.garminconnect` directory for persistent authentication.
+For security, ensure restrictive permissions:
+
+```bash
+chmod 700 ~/.garminconnect
+chmod 600 ~/.garminconnect/* 2>/dev/null || true
+```

188-199: Suggest using a separate token dir for tests to avoid clobbering real tokens.

 Create tokens in ~/.garminconnect by running the example program.
 
 ```bash
 # Install development dependencies
 pdm install --group :all

Run Tests:

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

+Optional: keep test tokens isolated
+
+bash +export GARMINTOKENS="$(mktemp -d)" +python ./example.py # create fresh tokens for tests +pdm run test +


---

`209-218`: **Avoid storing PyPI token in plaintext; add perms and env-var alternative.**


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

+bash +# Recommended: use environment variables and restrict file perms +chmod 600 ~/.pypirc +export TWINE_USERNAME="__token__" +export TWINE_PASSWORD="<PyPI_API_TOKEN>" +


---

`273-292`: **Harden the example against missing fields to prevent KeyError.**

`.get` makes the snippet safer across account/data variations.


```diff
-hr_data = client.get_heart_rates(_today)
-print(f"Resting HR: {hr_data['restingHeartRate']}")
+hr_data = client.get_heart_rates(_today)
+print(f"Resting HR: {hr_data.get('restingHeartRate', 'n/a')}")
garminconnect/__init__.py (4)

302-306: Reduce duplicate logging in download() exception path

logger.exception already includes the traceback; the extra logger.error duplicates the message. Compute status first and log once.

-            logger.exception("Download error path=%s", path)
-            status = getattr(getattr(e, "response", None), "status_code", None)
-            logger.error(
-                "Download failed for path '%s': %s (status=%s)", path, e, status
-            )
+            status = getattr(getattr(e, "response", None), "status_code", None)
+            logger.exception("Download failed for path '%s' (status=%s)", path, status)

37-40: Minor: prefer re.fullmatch for clarity

Anchors make it correct either way, but fullmatch conveys intent better.

-    if not re.match(DATE_FORMAT_REGEX, date_str):
+    if not re.fullmatch(DATE_FORMAT_REGEX, date_str):

1927-1936: Consider capping gear activities limit (or documenting server max)

Default 9999 might be rejected or slow. Optionally clamp to a sane ceiling or reuse MAX_ACTIVITY_LIMIT.

-        limit = _validate_positive_integer(limit, "limit")
+        limit = _validate_positive_integer(limit, "limit")
+        # Optional: enforce a reasonable ceiling to avoid heavy responses
+        limit = min(limit, MAX_ACTIVITY_LIMIT)

1739-1748: Validate goals status against allowed values

Avoid server-side 4xx by checking status locally.

         goals = []
         url = self.garmin_connect_goals_url
+        valid_statuses = {"active", "future", "past"}
+        if status not in valid_statuses:
+            raise ValueError(f"status must be one of {valid_statuses}")
         start = _validate_positive_integer(start, "start")
         limit = _validate_positive_integer(limit, "limit")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9553b9f and 8ea4374.

📒 Files selected for processing (3)
  • .pre-commit-config.yaml (1 hunks)
  • README.md (1 hunks)
  • garminconnect/__init__.py (45 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .pre-commit-config.yaml
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~46-~46: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~47-~47: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~48-~48: There might be a mistake here.
Context: ...major Garmin Connect features accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...error handling and user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash pip3 install...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...le ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~158-~158: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~162-~162: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~163-~163: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~164-~164: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~179-179: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~182-~182: There might be a mistake here.
Context: ...ersistent authentication. ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~205-~205: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~231-~231: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~235-~235: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~237-~237: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~240-~240: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~241-~241: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~294-~294: There might be a mistake here.
Context: ...tRate']}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~299-~299: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~303-~303: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~304-~304: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~305-~305: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~310-~310: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~314-~314: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~317-~317: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~318-~318: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~319-~319: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~321-~321: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~327-~327: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~328-~328: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~329-~329: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~332-~332: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
🔇 Additional comments (3)
garminconnect/__init__.py (3)

274-296: connectapi() hardening looks solid

Consistent status-based mapping (401/429/other) and guaranteed raise on HTTP errors. Good improvement over prior behavior.


1603-1613: upload_activity file handling fix is correct

Context manager and error mapping are appropriate. Nice cleanup.


313-384: login() return shape and flows are now consistent

Tuple return type, early-return on return_on_mfa, and post-login profile/settings load for the credential flow are handled correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
garminconnect/__init__.py (2)

2014-2016: Fix runtime TypeError: use tuple in isinstance, not union.

isinstance(payload, dict | list) raises at runtime. Switch to a tuple.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")

970-981: Normalize hydration timestamps; accept ISO with/without fractional seconds.

Current parsing is brittle and formats microseconds, not ms. Normalize via _fmt_ts and parse robustly.

@@
-        elif cdate is not None and timestamp is None:
-            # If cdate is not null, validate and use timestamp associated with midnight
-            cdate = _validate_date_format(cdate, "cdate")
-            try:
-                raw_ts = datetime.fromisoformat(cdate)
-            except ValueError:
-                # if cdate is just a date, parse with format and set time to 00:00:00
-                raw_ts = datetime.strptime(cdate, DATE_FORMAT_STR)
-            timestamp = raw_ts.replace(
-                hour=0, minute=0, second=0, microsecond=0
-            ).isoformat(timespec="microseconds")
+        elif cdate is not None and timestamp is None:
+            # If cdate is provided, validate and use midnight local time
+            cdate = _validate_date_format(cdate, "cdate")
+            raw_ts = datetime.strptime(cdate, DATE_FORMAT_STR)  # midnight local
+            timestamp = _fmt_ts(raw_ts)
@@
-        elif cdate is None and timestamp is not None:
-            # If timestamp is not null, validate and set cdate equal to date part of timestamp
-            if not isinstance(timestamp, str):
-                raise ValueError("timestamp must be a string")
-            try:
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
-                cdate = str(raw_ts.date())
-            except ValueError as e:
-                raise ValueError(
-                    f"Invalid timestamp format (expected YYYY-MM-DDTHH:MM:SS.mmm): {e}"
-                ) from e
+        elif cdate is None and timestamp is not None:
+            # If timestamp is provided, normalize and set cdate to its date part
+            if not isinstance(timestamp, str):
+                raise ValueError("timestamp must be a string")
+            try:
+                try:
+                    raw_ts = datetime.fromisoformat(timestamp)
+                except ValueError:
+                    raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S")
+                cdate = raw_ts.date().isoformat()
+                timestamp = _fmt_ts(raw_ts)
+            except ValueError as e:
+                raise ValueError("Invalid timestamp format (expected ISO 8601)") from e
@@
-        else:
-            # Both provided - validate consistency
-            cdate = _validate_date_format(cdate, "cdate")
-            if not isinstance(timestamp, str):
-                raise ValueError("timestamp must be a string")
-            try:
-                raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
-                ts_date = str(raw_ts.date())
-                if ts_date != cdate:
-                    raise ValueError(
-                        f"timestamp date ({ts_date}) doesn't match cdate ({cdate})"
-                    )
-            except ValueError as e:
-                if "doesn't match" in str(e):
-                    raise
-                raise ValueError(f"invalid timestamp format: {e}") from e
+        else:
+            # Both provided - validate consistency and normalize
+            cdate = _validate_date_format(cdate, "cdate")
+            if not isinstance(timestamp, str):
+                raise ValueError("timestamp must be a string")
+            try:
+                try:
+                    raw_ts = datetime.fromisoformat(timestamp)
+                except ValueError:
+                    raw_ts = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S")
+                ts_date = raw_ts.date().isoformat()
+                if ts_date != cdate:
+                    raise ValueError(
+                        f"timestamp date ({ts_date}) doesn't match cdate ({cdate})"
+                    )
+                timestamp = _fmt_ts(raw_ts)
+            except ValueError:
+                raise

Also applies to: 982-993, 994-1009

🧹 Nitpick comments (4)
garminconnect/__init__.py (3)

55-61: Reject booleans in numeric validators (bool is a subclass of int).

True/False currently pass as numbers; block them explicitly.

 def _validate_positive_number(
@@
-    if not isinstance(value, numbers.Real):
+    if not isinstance(value, numbers.Real):
         raise ValueError(f"{param_name} must be a number")
+    if isinstance(value, bool):
+        raise ValueError(f"{param_name} must be a number, not bool")
@@
 def _validate_non_negative_integer(value: int, param_name: str = "value") -> int:
@@
-    if not isinstance(value, int):
+    if not isinstance(value, int) or isinstance(value, bool):
         raise ValueError(f"{param_name} must be an integer")
@@
 def _validate_positive_integer(value: int, param_name: str = "value") -> int:
@@
-    if not isinstance(value, int):
+    if not isinstance(value, int) or isinstance(value, bool):
         raise ValueError(f"{param_name} must be an integer")

Also applies to: 66-71, 77-81


1526-1528: Doc fix: clarify timestamp precision in example.

Use 3-digit ms to match _fmt_ts and other endpoints.

-        start_datetime - timestamp in this pattern "2023-12-02T10:00:00.00"
+        start_datetime - timestamp in this pattern "2023-12-02T10:00:00.000"

1929-1933: Doc/default mismatch for limit in get_gear_activities.

Doc says default 9999; code uses 1000 (and min with MAX_ACTIVITY_LIMIT). Align the doc.

-        :param limit: Maximum number of activities to return (default: 9999)
+        :param limit: Maximum number of activities to return (default: 1000)
README.md (1)

30-30: Avoid staleness in “API Coverage Statistics” header.

The date will drift. Drop it or note “snapshot; may change per release”.

-## API Coverage Statistics (as of 2025-08-31)
+## API Coverage Statistics
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea4374 and 7a66df1.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • garminconnect/__init__.py (45 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~48-~48: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~50-~50: There might be a mistake here.
Context: ...r Garmin Connect features are accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~51-~51: There might be a mistake here.
Context: ...rror handling with user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~71-~71: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash python3 -m p...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ...n3 ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~161-~161: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~165-~165: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~166-~166: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~167-~167: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~182-182: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~183-~183: There might be a mistake here.
Context: ...directory for persistent authentication. For security, ensure restrictive permiss...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...* 2>/dev/null || true ``` ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~222-~222: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~256-~256: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~260-~260: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~261-~261: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~262-~262: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~265-~265: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~266-~266: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~319-~319: There might be a mistake here.
Context: ... 'n/a')}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~321-~321: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~328-~328: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~329-~329: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~330-~330: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~339-~339: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~342-~342: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~343-~343: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~344-~344: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~346-~346: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~352-~352: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~353-~353: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~354-~354: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
🔇 Additional comments (2)
garminconnect/__init__.py (2)

274-296: connectapi() wrapper hardening looks good.

Consistent error mapping (401/429/other), logging, and raising on all HTTP paths. Nice.


619-631: Timestamp ms normalization is correct here.

Using _fmt_ts for weigh-ins and blood pressure timestamps aligns with server expectations.

Also applies to: 772-795

Comment on lines 358 to 367
# Validate profile data exists
if not hasattr(self.garth, "profile") or not self.garth.profile:
raise GarminConnectAuthenticationError("Failed to retrieve profile")

self.display_name = self.garth.profile.get("displayName")
self.full_name = self.garth.profile.get("fullName")

if not self.display_name:
raise GarminConnectAuthenticationError("Invalid profile data found")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Tokenstore login can fail on profile check; fetch profile if missing.

When loading from tokenstore, garth.profile may be unset. Fallback to fetch profile instead of raising.

-            # Validate profile data exists
-            if not hasattr(self.garth, "profile") or not self.garth.profile:
-                raise GarminConnectAuthenticationError("Failed to retrieve profile")
-
-            self.display_name = self.garth.profile.get("displayName")
-            self.full_name = self.garth.profile.get("fullName")
+            # Ensure profile is loaded (tokenstore path may not populate it)
+            if not getattr(self.garth, "profile", None):
+                try:
+                    prof = self.garth.connectapi("/userprofile-service/userprofile/profile")
+                except Exception as e:
+                    raise GarminConnectAuthenticationError("Failed to retrieve profile") from e
+                if not prof or "displayName" not in prof:
+                    raise GarminConnectAuthenticationError("Invalid profile data found")
+                self.garth.profile = prof  # cache on client
+            self.display_name = self.garth.profile.get("displayName")
+            self.full_name = self.garth.profile.get("fullName")

Please verify tokenstore logins (with GARMINTOKENS) succeed after this change.


🏁 Script executed:

#!/bin/bash
# Display context around the profile validation snippet
sed -n '350,420p' garminconnect/__init__.py

# Locate the login method to see how profile is populated
rg -nP --type=py 'def\s+login' -C3 garminconnect/__init__.py

# Search for any references to tokenstore usage
rg -n 'tokenstore' -C3 .

Length of output: 6516


Fallback to fetch profile on tokenstore login: when self.garth.profile is empty after loading from tokenstore, call

prof = self.garth.connectapi("/userprofile-service/userprofile/profile")

cache it on self.garth.profile, then extract displayName/fullName, raising only if the fetch fails or returns no displayName.

🤖 Prompt for AI Agents
In garminconnect/__init__.py around lines 358 to 367, when self.garth.profile is
missing after loading from tokenstore, call
self.garth.connectapi("/userprofile-service/userprofile/profile"), assign the
result to self.garth.profile, then extract displayName and fullName from that
cached profile; only raise GarminConnectAuthenticationError if the connectapi
call fails/returns falsy or the resulting profile has no displayName, and wrap
the fetch in a try/except to raise GarminConnectAuthenticationError on errors.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
garminconnect/__init__.py (2)

2015-2017: Fix runtime TypeError in isinstance check.

isinstance(payload, dict | list) raises TypeError at runtime. Use a tuple form.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")

361-369: Tokenstore login can fail if profile isn’t preloaded — fetch profile as fallback.

Loading from a tokenstore may not set self.garth.profile. Proactively fetch it to avoid false AuthenticationError.

-            # Validate profile data exists
-            if not hasattr(self.garth, "profile") or not self.garth.profile:
-                raise GarminConnectAuthenticationError("Failed to retrieve profile")
-
-            self.display_name = self.garth.profile.get("displayName")
-            self.full_name = self.garth.profile.get("fullName")
-
-            if not self.display_name:
-                raise GarminConnectAuthenticationError("Invalid profile data found")
+            # Ensure profile is loaded (tokenstore path may not populate it)
+            if not getattr(self.garth, "profile", None):
+                try:
+                    prof = self.garth.connectapi("/userprofile-service/userprofile/profile")
+                except Exception as e:
+                    raise GarminConnectAuthenticationError("Failed to retrieve profile") from e
+                if not prof or "displayName" not in prof:
+                    raise GarminConnectAuthenticationError("Invalid profile data found")
+                self.garth.profile = prof  # cache on client
+            self.display_name = self.garth.profile.get("displayName")
+            self.full_name = self.garth.profile.get("fullName")
🧹 Nitpick comments (7)
garminconnect/__init__.py (5)

894-903: Make HR parsing resilient to Garmin’s typo and future fix.

Prefer the correct key and fall back to the historical typo “hearRate”.

-                # This is not a typo. The Garmin dictionary has a typo as of 2025-07-08, referring to it as "hearRate"
-                hr = entry.get("hearRate")
-                if hr is not None:
-                    speed_and_heart_rate_dict["heartRate"] = hr
-                    # Fix Garmin's typo
+                # Prefer correct key; fall back to Garmin's historical typo ("hearRate")
+                hr = entry.get("heartRate") or entry.get("hearRate")
+                if hr is not None:
+                    speed_and_heart_rate_dict["heartRate"] = hr

1833-1835: Nit: singularize log message.

Minor wording fix.

-        logger.debug("Downloading activities from %s", url)
+        logger.debug("Downloading activity from %s", url)

1783-1784: Nit: clarify log context (“gear defaults”).

Message currently implies general gear, not defaults.

-        logger.debug("Requesting gear for user %s", userProfileNumber)
+        logger.debug("Requesting gear defaults for user %s", userProfileNumber)

1972-1980: Docstring drift: ‘end’ → ‘limit’.

Signature uses limit, but docstring still says “till end”. Update to avoid confusion.

-    def get_workouts(self, start: int = 0, limit: int = 100) -> dict[str, Any]:
-        """Return workouts from start till end."""
+    def get_workouts(self, start: int = 0, limit: int = 100) -> dict[str, Any]:
+        """Return workouts starting at offset `start` with at most `limit` results."""

1979-1980: Query param type consistency (ints vs strings).

Some endpoints send params as strings, others as ints (e.g., get_workouts). Not a bug, but consider standardizing for consistency.

Also applies to: 1459-1462

README.md (2)

30-33: Add snapshot date to API coverage to prevent staleness confusion.

Explicitly timestamp the snapshot.

-## API Coverage Statistics
+## API Coverage Statistics (as of 2025-08-31)

226-235: Editor command may be OS-specific.

vi ~/.pypirc can confuse Windows users. Consider “use your editor” or show a here-doc example.

-vi ~/.pypirc
+# Edit with your preferred editor, or create via here-doc:
+# cat > ~/.pypirc <<'EOF'
+# [pypi]
+# username = __token__
+# password = <PyPI_API_TOKEN>
+# EOF
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7a66df1 and 6b362cb.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • garminconnect/__init__.py (45 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~48-~48: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~50-~50: There might be a mistake here.
Context: ...r Garmin Connect features are accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~51-~51: There might be a mistake here.
Context: ...rror handling with user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~71-~71: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash python3 -m p...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ...n3 ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~161-~161: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~165-~165: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~166-~166: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~167-~167: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~182-182: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~183-~183: There might be a mistake here.
Context: ...directory for persistent authentication. For security, ensure restrictive permiss...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...* 2>/dev/null || true ``` ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~222-~222: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~256-~256: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~260-~260: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~261-~261: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~262-~262: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~265-~265: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~266-~266: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~319-~319: There might be a mistake here.
Context: ... 'n/a')}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~321-~321: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~328-~328: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~329-~329: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~330-~330: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~339-~339: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~342-~342: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~343-~343: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~344-~344: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~346-~346: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~352-~352: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~353-~353: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~354-~354: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
🔇 Additional comments (1)
garminconnect/__init__.py (1)

1718-1722: Return type of activity types endpoint.

Double-check if /activity-service/activity/activityTypes returns a list. If so, adjust annotation to list[dict[str, Any]] for accuracy.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

2023-2024: Runtime bug: isinstance(payload, dict | list) raises TypeError.

Union types aren’t valid in isinstance at runtime. Use a tuple.

-        if not isinstance(payload, (dict | list)):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")
🧹 Nitpick comments (4)
README.md (1)

242-248: Fix accidental H1 heading in Publishing section (markdownlint).

The line starts with “#” but isn’t meant to be a top-level heading. Drop the hash to avoid MD001/MD022 noise.

-# Recommended: use environment variables and restrict file perms
+Recommended: use environment variables and restrict file perms
garminconnect/__init__.py (3)

655-663: Normalize gmtTimestamp to UTC; handle naive timestamps consistently.

If gmtTimestamp is provided without tzinfo, it’s treated as local/naive and may not be GMT. Convert to UTC explicitly and ensure both timestamps are ms-formatted via _fmt_ts.

-        dt = datetime.fromisoformat(dateTimestamp) if dateTimestamp else datetime.now()
-        dtGMT = (
-            datetime.fromisoformat(gmtTimestamp)
-            if gmtTimestamp
-            else dt.astimezone(timezone.utc)
-        )
+        # Make local timestamp timezone-aware
+        dt = (
+            datetime.fromisoformat(dateTimestamp).astimezone()
+            if dateTimestamp
+            else datetime.now().astimezone()
+        )
+        if gmtTimestamp:
+            g = datetime.fromisoformat(gmtTimestamp)
+            # Assume provided GMT is UTC if naive; otherwise convert to UTC
+            if g.tzinfo is None:
+                g = g.replace(tzinfo=timezone.utc)
+            dtGMT = g.astimezone(timezone.utc)
+        else:
+            dtGMT = dt.astimezone(timezone.utc)

1481-1487: Method name/docstring likely mismatched with endpoint (heartRate vs activities).

get_activities_fordate() calls “/mobile-gateway/heartRate/forDate” and the log says “activities”, which is misleading. Either adjust the name/docstring/log or switch to the intended activities endpoint.

-    def get_activities_fordate(self, fordate: str) -> dict[str, Any]:
-        """Return available activities for date."""
+    def get_activities_fordate(self, fordate: str) -> dict[str, Any]:
+        """Return heart rate data for the given date (legacy forDate path)."""
@@
-        logger.debug("Requesting activities for date %s", fordate)
+        logger.debug("Requesting heart rate (forDate) data for %s", fordate)

If this is truly meant to fetch activities-for-date, please replace the endpoint accordingly.


537-544: Guard against missing 'totalAverage' to avoid KeyError.

get_body_composition(cdate)["totalAverage"] can raise if the key is absent. Merge safely.

-        return {
-            **self.get_stats(cdate),
-            **self.get_body_composition(cdate)["totalAverage"],
-        }
+        stats = self.get_stats(cdate)
+        body = self.get_body_composition(cdate)
+        body_avg = body.get("totalAverage") or {}
+        if not isinstance(body_avg, dict):
+            body_avg = {}
+        return {**stats, **body_avg}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b362cb and c6846c9.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • garminconnect/__init__.py (45 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~48-~48: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~50-~50: There might be a mistake here.
Context: ...r Garmin Connect features are accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~51-~51: There might be a mistake here.
Context: ...rror handling with user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~71-~71: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash python3 -m p...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ...n3 ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~161-~161: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~165-~165: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~166-~166: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~167-~167: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~182-182: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~183-~183: There might be a mistake here.
Context: ...directory for persistent authentication. For security, ensure restrictive permiss...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...* 2>/dev/null || true ``` ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~222-~222: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~261-~261: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~265-~265: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~266-~266: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~267-~267: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~270-~270: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~271-~271: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ... 'n/a')}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~326-~326: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~329-~329: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~333-~333: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~334-~334: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~340-~340: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~344-~344: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~347-~347: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~348-~348: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~349-~349: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~351-~351: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~358-~358: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~359-~359: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~362-~362: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

2023-2034: Fix runtime TypeError: isinstance with union.

isinstance(payload, dict | list) raises TypeError at runtime. Use a tuple.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")
🧹 Nitpick comments (8)
README.md (4)

3-6: Clarify that counts are a snapshot to avoid drift.

“101 API methods” and “11 categories” will age quickly. Add “as of <version/date>” or link to a script that generates these numbers.

-The Garmin Connect API demo (`example.py`) provides comprehensive access to **101 API methods** organized into **11 categories** for easy navigation:
+The Garmin Connect API demo (`example.py`) provides comprehensive access to **101 API methods** organized into **11 categories** (snapshot as of vX.Y on YYYY-MM-DD) for easy navigation:

30-45: Mark “API Coverage Statistics” as generated or dated.

To prevent future mismatches, note the generation date/version or how to regenerate.

-## API Coverage Statistics
+## API Coverage Statistics (snapshot as of vX.Y on YYYY-MM-DD)

80-88: Offer a PDM-run alternative for consistency.

Since PDM is already installed, show an equivalent pdm-run to avoid accidental system interpreter use.

 python3 -m venv .venv --copies
 source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 pip install pdm
 pdm install --group :example
-python3 ./example.py
+pdm run python ./example.py

324-328: Prefer repository link over raw URL for readability.

Use a browsable link (and keep raw link as a secondary bullet if desired).

-- **Source Code**: [example.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/example.py)
+- **Source Code**: [example.py](https://github.com/cyberjunky/python-garminconnect/blob/master/example.py)
garminconnect/__init__.py (4)

327-333: Tokenstore detection heuristic could misclassify.

Length > 512 is brittle. Prefer “path exists → load(), else → loads()” and fall back if JSON parse fails.

-            if tokenstore:
-                if len(tokenstore) > 512:
-                    self.garth.loads(tokenstore)
-                else:
-                    self.garth.load(tokenstore)
+            if tokenstore:
+                try:
+                    if Path(tokenstore).exists():
+                        self.garth.load(tokenstore)
+                    else:
+                        self.garth.loads(tokenstore)
+                except Exception:
+                    # Final fallback: attempt loads()
+                    self.garth.loads(tokenstore)

836-847: delete_blood_pressure(): validate cdate to prevent 4xx.

Add date validation like other endpoints.

-    def delete_blood_pressure(self, version: str, cdate: str) -> dict[str, Any]:
+    def delete_blood_pressure(self, version: str, cdate: str) -> dict[str, Any]:
+        cdate = _validate_date_format(cdate, "cdate")
         url = f"{self.garmin_connect_set_blood_pressure_endpoint}/{cdate}/{version}"

1586-1632: upload_activity(): simplify extension parsing and tighten enum check.

Use Path.suffix and early-normalize.

-        file_parts = file_base_name.split(".")
-        if len(file_parts) < 2:
+        if not p.suffix:
             raise GarminConnectInvalidFileFormatError(
                 f"File has no extension: {activity_path}"
             )
-
-        file_extension = file_parts[-1]
-        allowed_file_extension = (
-            file_extension.upper() in Garmin.ActivityUploadFormat.__members__
-        )
+        file_extension = p.suffix.lstrip(".").upper()
+        allowed_file_extension = file_extension in Garmin.ActivityUploadFormat.__members__

1702-1734: Boolean query param should be lowercase “true/false”.

Other calls use "true"; this sends "True"/"False". Normalize to lowercase string for consistency.

-            "groupByParentActivityType": str(groupbyactivities),
+            "groupByParentActivityType": str(groupbyactivities).lower(),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c6846c9 and 9ea366a.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • garminconnect/__init__.py (44 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~48-~48: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~50-~50: There might be a mistake here.
Context: ...r Garmin Connect features are accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~51-~51: There might be a mistake here.
Context: ...rror handling with user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~59-~59: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~71-~71: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash python3 -m p...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ...n3 ./example.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~161-~161: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~165-~165: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~166-~166: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~167-~167: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~182-182: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~183-~183: There might be a mistake here.
Context: ...directory for persistent authentication. For security, ensure restrictive permiss...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...* 2>/dev/null || true ``` ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~222-~222: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~261-~261: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~265-~265: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~266-~266: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~267-~267: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~270-~270: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~271-~271: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ... 'n/a')}") ``` ### Additional Resources - Source Code: [example.py](https://raw....

(QB_NEW_EN)


[grammar] ~326-~326: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~329-~329: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~333-~333: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~334-~334: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~340-~340: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~344-~344: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~347-~347: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~348-~348: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~349-~349: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~351-~351: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~358-~358: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~359-~359: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~362-~362: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
🔇 Additional comments (14)
README.md (1)

7-28: Menu example looks great.

The dynamic main menu sample is clear and helps readers understand the UX.

garminconnect/__init__.py (13)

277-299: connectapi(): solid HTTP error mapping and logging.

Good separation of 401/429 and a catch‑all that raises; logger.exception on generic errors is helpful.


300-312: download(): consistent with connectapi().

Mirrors the error handling pattern appropriately.


313-321: Login return contract documented; tuple shape consistent.

Docstring clarifies (tokenstore) None/None vs credential flow. Looks good.


361-391: Profile/settings fallback is reasonable.

Fetching profile when missing from tokenstore and then loading settings is a good hardening step.


489-507: Date-range validation LGTM.

Good guard against start > end.


615-645: Weigh-in timestamps: correct local/UTC pairing and ms precision.

Matches API expectations and is consistent with _fmt_ts.


646-690: add_weigh_in_with_timestamps: timezone handling is robust.

Handles naive/aware inputs and normalizes to UTC correctly.


781-816: Blood pressure validation ranges: good bounds and type checks.

Clear client-side validation before POST.


958-1036: Hydration input normalization is careful and consistent.

ISO parsing with fallback and ms formatting looks good; reasonable cap via MAX_HYDRATION_ML.


1411-1431: get_device_solar_data(): strong input checks and return type alignment.

Validates dates and ensures expected subfield presence.


1989-1997: get_workouts(): validated pagination and params LGTM.

Clear contract and bounds.


2067-2091: GraphQL helper: useful debug context, returns parsed JSON.

Good addition; interface is minimal and pragmatic.


2092-2097: Deprecation notice level is appropriate.

Switch to warning is correct.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

2100-2111: Runtime TypeError: isinstance(payload, dict | list).

Union types aren’t valid in isinstance. Use a tuple.

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")
🧹 Nitpick comments (6)
README.md (2)

33-48: Make snapshot date explicit to avoid drift.

The “101 methods / 11 categories” snapshot will age. Append an “as of YYYY-MM-DD” date so readers know when the counts were taken.

-- **Total API Methods**: 101 unique endpoints (snapshot)
+- **Total API Methods**: 101 unique endpoints (snapshot as of 2025-09-06)

332-337: Link labels suggest exact counts; soften wording.

“All 101 API methods” can go stale when APIs change. Prefer “All current API methods.”

-- **Comprehensive Demo**: [demo.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/demo.py) - All 101 API methods
+- **Comprehensive Demo**: [demo.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/demo.py) - All current API methods
pyproject.toml (3)

87-89: Remove hard-coded Unix venv path (breaks on Windows).

PDM discovers the venv automatically; this setting is unnecessary and OS-specific.

-[tool.pdm.python]
-path = ".venv/bin/python"

36-38: Stabilize pytest-vcr by ignoring SSO host (prevents cassette churn).

If CI churns on SSO with record=once, ignore sso.garmin.com at the pytest level.

 [tool.pytest.ini_options]
 addopts = "--ignore=__pypackages__ --ignore-glob=*.yaml"
+vcr_record = "once"
+vcr_filter_headers = ["authorization", "cookie", "user-agent"]
+vcr_ignore_hosts = ["sso.garmin.com"]

166-167: Make clean-vcr cross-platform.

rm -f is not portable. Use Python to delete cassette files.

-clean-vcr = "rm -f tests/cassettes/*.yaml"
+clean-vcr = "python -c \"import pathlib; p=pathlib.Path('tests/cassettes'); [f.unlink() for f in p.glob('*.yaml')]\""
example.py (1)

106-116: Don’t conflate auth/connection errors with “no tokens” message.

Return specific messages for auth and connection to aid troubleshooting.

-    except (
-        FileNotFoundError,
-        GarminConnectAuthenticationError,
-        GarminConnectConnectionError,
-    ):
-        return (
-            False,
-            None,
-            "No valid tokens found. Please run with your email/password credentials to create new tokens.",
-        )
+    except FileNotFoundError:
+        return False, None, "No valid tokens found. Run with your email/password to create new tokens."
+    except GarminConnectAuthenticationError as e:
+        return False, None, f"Authentication issue: {e}"
+    except GarminConnectConnectionError as e:
+        return False, None, f"Connection issue: {e}"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ea366a and ec1e501.

📒 Files selected for processing (4)
  • README.md (1 hunks)
  • example.py (2 hunks)
  • garminconnect/__init__.py (44 hunks)
  • pyproject.toml (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
example.py (2)
demo.py (3)
  • safe_api_call (948-1031)
  • init_api (3385-3478)
  • main (3481-3587)
garminconnect/__init__.py (10)
  • GarminConnectAuthenticationError (2185-2186)
  • GarminConnectConnectionError (2177-2178)
  • GarminConnectTooManyRequestsError (2181-2182)
  • Garmin (104-2174)
  • login (353-476)
  • resume_login (478-492)
  • get_full_name (494-497)
  • get_device_last_used (1523-1529)
  • get_user_summary (512-530)
  • get_hydration_data (1114-1121)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
🪛 LanguageTool
README.md

[grammar] ~51-~51: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~52-~52: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~53-~53: There might be a mistake here.
Context: ...r Garmin Connect features are accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~54-~54: There might be a mistake here.
Context: ...rror handling with user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~62-~62: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~67-~67: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~68-~68: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~74-~74: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash python3 -m p...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...thon3 ./demo.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~169-~169: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~173-~173: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~174-~174: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~175-~175: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~190-190: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...directory for persistent authentication. For security, ensure restrictive permiss...

(QB_NEW_EN)


[grammar] ~199-~199: There might be a mistake here.
Context: ...* 2>/dev/null || true ``` ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~230-~230: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~269-~269: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~273-~273: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~274-~274: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~275-~275: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~278-~278: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~279-~279: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~332-~332: There might be a mistake here.
Context: ... 'n/a')}") ``` ### Additional Resources - Simple Example: [example.py](https://r...

(QB_NEW_EN)


[grammar] ~333-~333: There might be a mistake here.
Context: ...ster/example.py) - Getting started guide - Comprehensive Demo: [demo.py](https://...

(QB_NEW_EN)


[grammar] ~334-~334: There might be a mistake here.
Context: ...ct/master/demo.py) - All 101 API methods - API Documentation: Comprehensive metho...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~338-~338: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~342-~342: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~343-~343: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~344-~344: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~349-~349: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~353-~353: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~356-~356: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~358-~358: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~360-~360: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~366-~366: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~367-~367: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~368-~368: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~371-~371: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
🔇 Additional comments (1)
garminconnect/__init__.py (1)

289-323: Error mapping looks solid.

connectapi/download now classify 401/429 and 4xx vs others with logging and re-raise. Good improvement over silent None returns.

Comment on lines +125 to 133
"""Get email and password from environment or user input."""
email = os.getenv("EMAIL")
password = os.getenv("PASSWORD")

email = input("Login e-mail: ")
password = getpass("Enter password: ")
if not email:
email = input("Login email: ")
if not password:
password = getpass("Enter password: ")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unify environment variables with README (support GARMIN_ and fallback).*

README uses GARMIN_EMAIL/PASSWORD; this example uses EMAIL/PASSWORD. Support both to avoid confusion.

-    email = os.getenv("EMAIL")
-    password = os.getenv("PASSWORD")
+    # Prefer explicit GARMIN_*; fallback to generic names for compatibility
+    email = os.getenv("GARMIN_EMAIL") or os.getenv("EMAIL")
+    password = os.getenv("GARMIN_PASSWORD") or os.getenv("PASSWORD")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""Get email and password from environment or user input."""
email = os.getenv("EMAIL")
password = os.getenv("PASSWORD")
email = input("Login e-mail: ")
password = getpass("Enter password: ")
if not email:
email = input("Login email: ")
if not password:
password = getpass("Enter password: ")
"""Get email and password from environment or user input."""
# Prefer explicit GARMIN_*; fallback to generic names for compatibility
email = os.getenv("GARMIN_EMAIL") or os.getenv("EMAIL")
password = os.getenv("GARMIN_PASSWORD") or os.getenv("PASSWORD")
if not email:
email = input("Login email: ")
if not password:
password = getpass("Enter password: ")
🤖 Prompt for AI Agents
In example.py around lines 125 to 133, the code only reads EMAIL/PASSWORD from
the environment but README uses GARMIN_EMAIL/GARMIN_PASSWORD; update the
environment lookup to prefer GARMIN_EMAIL and GARMIN_PASSWORD and fall back to
EMAIL and PASSWORD (e.g., email = os.getenv("GARMIN_EMAIL") or
os.getenv("EMAIL")) so both variable names are supported, then keep the existing
interactive input fallback if neither env var is set.

# Get credentials
email, password = get_credentials()

print("� Logging in with credentials...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix garbled character in user-facing log line.

The leading replacement character breaks output. Use a proper emoji or plain text.

-            print("� Logging in with credentials...")
+            print("🔐 Logging in with credentials...")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(" Logging in with credentials...")
print("🔐 Logging in with credentials...")
🤖 Prompt for AI Agents
In example.py around line 181, the user-facing print contains a garbled
replacement character ("� Logging in with credentials..."); replace it with a
valid emoji (e.g., "🔐 Logging in with credentials...") or plain text ("Logging
in with credentials...") to avoid the broken character, update the string
literal accordingly, and ensure the file encoding is UTF-8 so the emoji renders
correctly.

Comment on lines +484 to 491
if self.garth.profile:
self.display_name = self.garth.profile["displayName"]
self.full_name = self.garth.profile["fullName"]

settings = self.garth.connectapi(self.garmin_connect_user_settings_url)
self.unit_system = settings["userData"]["measurementSystem"]
if settings and "userData" in settings:
self.unit_system = settings["userData"]["measurementSystem"]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

resume_login may leave display_name unset when garth.profile is missing.

If garth doesn’t populate profile on MFA resume, subsequent calls using display_name will fail. Fetch profile as a fallback (mirror login()).

-        if self.garth.profile:
-            self.display_name = self.garth.profile["displayName"]
-            self.full_name = self.garth.profile["fullName"]
+        if getattr(self.garth, "profile", None):
+            self.display_name = self.garth.profile.get("displayName")
+            self.full_name = self.garth.profile.get("fullName")
+        else:
+            try:
+                prof = self.garth.connectapi("/userprofile-service/userprofile/profile")
+            except Exception as e:
+                raise GarminConnectAuthenticationError("Failed to retrieve profile") from e
+            if not prof or "displayName" not in prof:
+                raise GarminConnectAuthenticationError("Invalid profile data found")
+            self.display_name = prof.get("displayName")
+            self.full_name = prof.get("fullName")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if self.garth.profile:
self.display_name = self.garth.profile["displayName"]
self.full_name = self.garth.profile["fullName"]
settings = self.garth.connectapi(self.garmin_connect_user_settings_url)
self.unit_system = settings["userData"]["measurementSystem"]
if settings and "userData" in settings:
self.unit_system = settings["userData"]["measurementSystem"]
# Populate display_name/full_name, fetching profile if missing
- if self.garth.profile:
- self.display_name = self.garth.profile["displayName"]
if getattr(self.garth, "profile", None):
self.display_name = self.garth.profile.get("displayName")
self.full_name = self.garth.profile.get("fullName")
else:
try:
prof = self.garth.connectapi("/userprofile-service/userprofile/profile")
except Exception as e:
raise GarminConnectAuthenticationError("Failed to retrieve profile") from e
if not prof or "displayName" not in prof:
raise GarminConnectAuthenticationError("Invalid profile data found")
self.display_name = prof.get("displayName")
self.full_name = prof.get("fullName")
settings = self.garth.connectapi(self.garmin_connect_user_settings_url)
if settings and "userData" in settings:
self.unit_system = settings["userData"]["measurementSystem"]
🤖 Prompt for AI Agents
In garminconnect/__init__.py around lines 484-491, resume_login can leave
self.display_name unset when self.garth.profile is missing; replicate the
login() behavior by fetching the profile as a fallback: if self.garth.profile is
falsy then call the same profile API used in login() to populate
self.garth.profile (or assign a local profile dict), then set self.display_name
and self.full_name from that profile (with safe key checks/defaults) before
proceeding; this ensures display_name is always set after resume_login even when
MFA prevented initial profile population.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
pyproject.toml (2)

80-85: Excludes look good; prior packaging nit resolved.

The PDM build excludes now omit tests, test_data, and .github as suggested.


102-114: Ruff: Bandit rules enabled and per-file ignore consistent.

“S” is selected and tests/* ignores S101—this aligns with the earlier note.

Also applies to: 122-124

garminconnect/__init__.py (2)

2099-2101: Runtime bug: isinstance with union raises TypeError at runtime

Use a tuple for isinstance checks.

Apply:

-        if not isinstance(payload, dict | list):
+        if not isinstance(payload, (dict, list)):
             raise ValueError("workout_json must be a JSON object or array")

474-481: resume_login may leave display_name unset; add profile fallback

If garth.profile isn’t populated, subsequent calls using display_name will fail. Fetch profile as a fallback.

Apply:

-        if self.garth.profile:
-            self.display_name = self.garth.profile["displayName"]
-            self.full_name = self.garth.profile["fullName"]
+        if getattr(self.garth, "profile", None):
+            self.display_name = self.garth.profile.get("displayName")
+            self.full_name = self.garth.profile.get("fullName")
+        else:
+            try:
+                prof = self.garth.connectapi("/userprofile-service/userprofile/profile")
+            except Exception as e:
+                raise GarminConnectAuthenticationError("Failed to retrieve profile") from e
+            if not prof or "displayName" not in prof:
+                raise GarminConnectAuthenticationError("Invalid profile data found")
+            self.display_name = prof.get("displayName")
+            self.full_name = prof.get("fullName")
🧹 Nitpick comments (10)
pyproject.toml (7)

145-149: Put Ruff first (done) but drop --unsafe-fixes for safety.

Nice reordering. Consider removing --unsafe-fixes in the format task to avoid risky rewrites in a bulk formatter step.

-format = {composite = ["pdm run ruff check . --fix --unsafe-fixes", "pdm run isort . --skip-gitignore", "pdm run black -l 88 ."]}
+format = {composite = ["pdm run ruff check . --fix", "pdm run isort . --skip-gitignore", "pdm run black -l 88 ."]}

87-88: Remove hard-coded interpreter path; breaks Windows/dev portability.

Let PDM resolve Python automatically or set via PDM config, not a Unix-only path.

-[tool.pdm.python]
-path = ".venv/bin/python"

72-74: Avoid duplicating core deps in extras to prevent drift.

garth is already a main dependency; keeping it in the example extra can diverge over time.

 [project.optional-dependencies]
 example = [
-    "garth>=0.5.17,<0.6.0",
     "requests",
     "readchar",
 ]

166-166: Make clean-vcr cross-platform.

rm fails on Windows; use a tiny Python one-liner instead.

-clean-vcr = "rm -f tests/cassettes/*.yaml"
+clean-vcr = "python -c \"import pathlib; [p.unlink(missing_ok=True) for p in pathlib.Path('tests/cassettes').glob('*.yaml')]\""

20-24: Classifier nit: OS Independent + specific OS is redundant.

Either keep OS-specific triplet or just “OS Independent”. Not a blocker.

-    "Operating System :: MacOS :: MacOS X",
-    "Operating System :: Microsoft :: Windows",
-    "Operating System :: POSIX :: Linux",
-    "Operating System :: OS Independent",
+    "Operating System :: OS Independent",

59-64: Minor: duplicate tool lists between extras and dev groups.

You maintain both optional-deps “linting” and dev-deps “linting”; that’s fine, but keep them in sync or consolidate to one to avoid drift.

Also applies to: 181-188


36-38: Add pytest-vcr config to ignore SSO and filter auth headers
Include the following under [tool.pytest.ini_options] to stabilize cassettes and prevent CannotOverwriteExistingCassetteException:

 [tool.pytest.ini_options]
 addopts = "--ignore=__pypackages__ --ignore-glob=*.yaml"
+vcr_record = "once"
+vcr_filter_headers = ["authorization", "cookie", "user-agent"]
+vcr_ignore_hosts = ["sso.garmin.com"]
garminconnect/__init__.py (3)

695-705: Make timestamp timezone-aware before UTC conversion (align with add_weigh_in_with_timestamps)

Prevents edge cases with naive datetimes.

Apply:

-        try:
-            dt = datetime.fromisoformat(timestamp) if timestamp else datetime.now()
+        try:
+            dt = (
+                datetime.fromisoformat(timestamp).astimezone()
+                if timestamp
+                else datetime.now().astimezone()
+            )

942-951: Normalize sport param casing (“RUNNING”) for consistency

Other calls use RUNNING; this one uses Running. Standardize to reduce surprises; confirm server behavior.

Apply:

-            power_url = f"{self.garmin_connect_biometric_url}/powerToWeight/latest/{date.today()}?sport=Running"
+            power_url = f"{self.garmin_connect_biometric_url}/powerToWeight/latest/{date.today()}?sport=RUNNING"

Would you confirm which case Garmin expects for this endpoint in your tests?

Also applies to: 1013-1018


2134-2157: Validate GraphQL payload type at runtime

Defensive check prevents accidental non-dict payloads from being posted.

Apply:

     def query_garmin_graphql(self, query: dict[str, Any]) -> dict[str, Any]:
         """Execute a POST to Garmin's GraphQL endpoint.
@@
-        op = (
+        if not isinstance(query, dict):
+            raise TypeError("query must be a dict")
+        op = (
             (query.get("operationName") or "unnamed")
             if isinstance(query, dict)
             else "unnamed"
         )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec1e501 and f5d1d19.

📒 Files selected for processing (2)
  • garminconnect/__init__.py (44 hunks)
  • pyproject.toml (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
garminconnect/__init__.py (1)
garminconnect/fit.py (2)
  • FitEncoderWeight (465-517)
  • timestamp (410-415)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
🔇 Additional comments (3)
pyproject.toml (1)

80-85: Ensure garminconnect/py.typed exists and is included in the PDM build

I didn’t find a py.typed marker file in the repo; please verify it’s present under garminconnect/ and update your pyproject.toml:

 [tool.pdm.build]
 excludes = [
   "tests/**",
   "test_data/**",
   ".github/**",
 ]
+includes = ["garminconnect/py.typed"]
garminconnect/__init__.py (2)

279-313: connectapi error mapping looks solid

Clear 401/429 handling, client vs other HTTP errors, and preserved tracebacks. LGTM.


314-341: download error handling aligned with connectapi

Good consistency and logging. LGTM.

@cyberjunky cyberjunky merged commit ccfa52f into master Sep 6, 2025
10 of 11 checks passed
@cyberjunky cyberjunky deleted the revamp branch September 6, 2025 10:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
example.py (3)

18-21: Align env var names with README (support GARMIN_ with fallback).*

Prefer GARMIN_EMAIL/GARMIN_PASSWORD while retaining EMAIL/PASSWORD for compatibility; update docs accordingly.

-Environment Variables (optional):
-export EMAIL=<your garmin email address>
-export PASSWORD=<your garmin password>
-export GARMINTOKENS=<path to token storage>
+Environment Variables (optional):
+# Prefer GARMIN_*; fallback to generic names
+export GARMIN_EMAIL=<your garmin email address>
+export GARMIN_PASSWORD=<your garmin password>
+export EMAIL=<your garmin email address>
+export PASSWORD=<your garmin password>
+export GARMINTOKENS=<path to token storage>

126-135: Read GARMIN_ env vars and fallback to EMAIL/PASSWORD.*

Make the example consistent with README and past reviews.

-    email = os.getenv("EMAIL")
-    password = os.getenv("PASSWORD")
+    # Prefer explicit GARMIN_*; fallback to generic names
+    email = os.getenv("GARMIN_EMAIL") or os.getenv("EMAIL")
+    password = os.getenv("GARMIN_PASSWORD") or os.getenv("PASSWORD")

183-183: Fix garbled character in log line.

Use a valid emoji or plain text.

-            print("� Logging in with credentials...")
+            print("🔐 Logging in with credentials...")
+# or: print("Logging in with credentials...")
🧹 Nitpick comments (6)
example.py (3)

56-105: Harden HTTP error mapping (avoid substring status checks).

Rely on response.status_code; parsing substrings risks false matches.

-    except GarthHTTPError as e:
-        # Handle specific HTTP errors gracefully
-        error_str = str(e)
-        status_code = getattr(getattr(e, "response", None), "status_code", None)
+    except GarthHTTPError as e:
+        # Handle specific HTTP errors gracefully
+        status_code = getattr(getattr(e, "response", None), "status_code", None)
+        if status_code is None:
+            # Best effort: try to extract a 3-digit code from the exception repr
+            import re
+            m = re.search(r'\b(4\d{2}|5\d{2})\b', repr(e))
+            status_code = int(m.group(1)) if m else None
@@
-        if status_code == 400 or "400" in error_str:
+        if status_code == 400:
@@
-        elif status_code == 401 or "401" in error_str:
+        elif status_code == 401:
@@
-        elif status_code == 403 or "403" in error_str:
+        elif status_code == 403:
@@
-        elif status_code == 404 or "404" in error_str:
+        elif status_code == 404:
@@
-        elif status_code == 429 or "429" in error_str:
+        elif status_code == 429:
@@
-        elif status_code == 500 or "500" in error_str:
+        elif status_code == 500:
@@
-        elif status_code == 503 or "503" in error_str:
+        elif status_code == 503:

220-223: Ensure token directory exists before saving.

Without this, dump can fail on first-run.

-            garmin.garth.dump(str(tokenstore_path))
+            # Ensure token directory exists on first run
+            tokenstore_path.mkdir(parents=True, exist_ok=True)
+            garmin.garth.dump(str(tokenstore_path))

308-318: Show hydration when value is 0 ml.

Current truthy check hides valid zero values.

-    if success and hydration and hydration.get("valueInML"):
+    if success and hydration is not None and "valueInML" in hydration:
README.md (3)

33-33: Mark coverage counts as snapshot to avoid staleness.

-## API Coverage Statistics
+## API Coverage Statistics (snapshot)

60-60: Minor wording: “Python 3”.

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

334-335: Avoid hard-coding the exact method count.

Use “100+” to prevent future drift.

-- **Comprehensive Demo**: [demo.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/demo.py) - All 101 API methods
+- **Comprehensive Demo**: [demo.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/demo.py) - 100+ API methods
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5d1d19 and 4addc9a.

📒 Files selected for processing (3)
  • README.md (1 hunks)
  • example.py (2 hunks)
  • pyproject.toml (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
🧬 Code graph analysis (1)
example.py (2)
demo.py (2)
  • safe_api_call (948-1031)
  • init_api (3385-3478)
garminconnect/__init__.py (14)
  • GarminConnectAuthenticationError (2175-2176)
  • GarminConnectConnectionError (2167-2168)
  • GarminConnectTooManyRequestsError (2171-2172)
  • Garmin (94-2164)
  • login (343-466)
  • resume_login (468-482)
  • get_full_name (484-487)
  • get_device_last_used (1513-1519)
  • get_user_summary (502-520)
  • get_hydration_data (1104-1111)
  • Garmin (16-1571)
  • __init__ (19-221)
  • resume_login (269-279)
  • login (229-267)
🪛 LanguageTool
README.md

[grammar] ~51-~51: There might be a mistake here.
Context: ...gorized navigation with emoji indicators - Smart Data Management: Interactive wei...

(QB_NEW_EN)


[grammar] ~52-~52: There might be a mistake here.
Context: ...igh-in deletion with search capabilities - Comprehensive Coverage: All major Garm...

(QB_NEW_EN)


[grammar] ~53-~53: There might be a mistake here.
Context: ...r Garmin Connect features are accessible - Error Handling: Robust error handling ...

(QB_NEW_EN)


[grammar] ~54-~54: There might be a mistake here.
Context: ...rror handling with user-friendly prompts - Data Export: JSON export functionality...

(QB_NEW_EN)


[grammar] ~60-~60: Ensure spelling is correct
Context: ...m/sponsors/cyberjunky) A comprehensive Python3 API wrapper for Garmin Connect, providi...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~62-~62: There might be a mistake here.
Context: ..., fitness, and device data. ## 📖 About This library enables developers to progr...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...eep, stress, body composition, SpO2, HRV - Activity Data: Workouts, exercises, tr...

(QB_NEW_EN)


[grammar] ~67-~67: There might be a mistake here.
Context: ...es, training status, performance metrics - Device Information: Connected devices,...

(QB_NEW_EN)


[grammar] ~68-~68: There might be a mistake here.
Context: ...ed devices, settings, alarms, solar data - Goals & Achievements: Personal records...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: ...ds, badges, challenges, race predictions - Historical Data: Trends, progress trac...

(QB_NEW_EN)


[grammar] ~74-~74: There might be a mistake here.
Context: ...connect.garmin.com/> ## 📦 Installation Install from PyPI: ```bash python3 -m p...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...thon3 ./demo.py ``` ## 🛠️ Development Set up a development environment for con...

(QB_NEW_EN)


[grammar] ~169-~169: There might be a mistake here.
Context: ...quality standards. ## 🔐 Authentication The library uses the same OAuth authenti...

(QB_NEW_EN)


[grammar] ~173-~173: There might be a mistake here.
Context: ...ithub.com/matin/garth). Key Features: - Login credentials valid for one year (no...

(QB_NEW_EN)


[grammar] ~174-~174: There might be a mistake here.
Context: ... valid for one year (no repeated logins) - Secure OAuth token storage - Same authen...

(QB_NEW_EN)


[grammar] ~175-~175: There might be a mistake here.
Context: ...ted logins) - Secure OAuth token storage - Same authentication flow as official app...

(QB_NEW_EN)


[grammar] ~190-190: There might be a mistake here.
Context: ...e non-sensitive. ``` Token Storage: Tokens are automatically saved to `
/.ga...

(QB_NEW_EN)


[grammar] ~191-~191: There might be a mistake here.
Context: ...directory for persistent authentication. For security, ensure restrictive permiss...

(QB_NEW_EN)


[grammar] ~199-~199: There might be a mistake here.
Context: ...* 2>/dev/null || true ``` ## 🧪 Testing Run the test suite to verify functionali...

(QB_NEW_EN)


[grammar] ~230-~230: There might be a mistake here.
Context: ... in ~/.garminconnect ## 📦 Publishing For package maintainers: **Setup PyPI c...

(QB_NEW_EN)


[grammar] ~269-~269: There might be a mistake here.
Context: ...re-built package ``` ## 🤝 Contributing We welcome contributions! Here's how you...

(QB_NEW_EN)


[grammar] ~273-~273: There might be a mistake here.
Context: ...s and feature requests via GitHub issues - Submit PRs: Code improvements, new fea...

(QB_NEW_EN)


[grammar] ~274-~274: There might be a mistake here.
Context: ...nts, new features, documentation updates - Testing: Help test new features and re...

(QB_NEW_EN)


[grammar] ~275-~275: There might be a mistake here.
Context: ...features and report compatibility issues - Documentation: Improve examples, add u...

(QB_NEW_EN)


[grammar] ~278-~278: There might be a mistake here.
Context: ...cases, fix typos Before Contributing: 1. Set up development environment (`pdm ins...

(QB_NEW_EN)


[grammar] ~279-~279: There might be a mistake here.
Context: ... typos Before Contributing: 1. Set up development environment (`pdm install -...

(QB_NEW_EN)


[grammar] ~332-~332: There might be a mistake here.
Context: ... 'n/a')}") ``` ### Additional Resources - Simple Example: [example.py](https://r...

(QB_NEW_EN)


[grammar] ~333-~333: There might be a mistake here.
Context: ...ster/example.py) - Getting started guide - Comprehensive Demo: [demo.py](https://...

(QB_NEW_EN)


[grammar] ~334-~334: There might be a mistake here.
Context: ...ct/master/demo.py) - All 101 API methods - API Documentation: Comprehensive metho...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...sive method documentation in source code - Test Cases: Real-world usage examples ...

(QB_NEW_EN)


[grammar] ~338-~338: There might be a mistake here.
Context: ...tests/` directory ## 🙏 Acknowledgments Special thanks to all contributors who h...

(QB_NEW_EN)


[grammar] ~342-~342: There might be a mistake here.
Context: ... feature requests, and code improvements - Issue Reporters: Helping identify and ...

(QB_NEW_EN)


[grammar] ~343-~343: There might be a mistake here.
Context: ...dentify and resolve compatibility issues - Feature Developers: Adding new API end...

(QB_NEW_EN)


[grammar] ~344-~344: There might be a mistake here.
Context: ...ding new API endpoints and functionality - Documentation Authors: Improving examp...

(QB_NEW_EN)


[grammar] ~349-~349: There might be a mistake here.
Context: ...nd feedback. ## 💖 Support This Project If you find this library useful for your...

(QB_NEW_EN)


[grammar] ~353-~353: There might be a mistake here.
Context: ...and maintenance: ### 🌟 Ways to Support - ⭐ Star this repository - Help others d...

(QB_NEW_EN)


[grammar] ~356-~356: There might be a mistake here.
Context: ...tribute to development and hosting costs - 🐛 Report Issues - Help improve stabil...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ...Help improve stability and compatibility - 📖 Spread the Word - Share with other ...

(QB_NEW_EN)


[grammar] ~358-~358: There might be a mistake here.
Context: ...the Word** - Share with other developers ### 💳 Financial Support Options [![Donate ...

(QB_NEW_EN)


[grammar] ~360-~360: There might be a mistake here.
Context: ...lopers ### 💳 Financial Support Options Donate via PayPal Sponsor on GitHub Why Support? - Keeps the project activ...

(QB_NEW_EN)


[grammar] ~366-~366: There might be a mistake here.
Context: ... - Keeps the project actively maintained - Enables faster bug fixes and new feature...

(QB_NEW_EN)


[grammar] ~367-~367: There might be a mistake here.
Context: ...nables faster bug fixes and new features - Supports infrastructure costs (testing, ...

(QB_NEW_EN)


[grammar] ~368-~368: There might be a mistake here.
Context: ...nfrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours...

(QB_NEW_EN)


[grammar] ~371-~371: There might be a mistake here.
Context: ...ifference and is greatly appreciated! 🙏

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.11)
🔇 Additional comments (2)
example.py (1)

161-176: Nice token-first login flow.

Graceful fallback from stored tokens to credentials is clear and user-friendly.

README.md (1)

178-188: Good: OAuth example uses env vars for placeholders.

This reduces secret-scanner noise and models safe practices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants