Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cyberjunky/python-garminconnect
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.3.3
Choose a base ref
...
head repository: cyberjunky/python-garminconnect
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.3.4
Choose a head ref
  • 7 commits
  • 6 files changed
  • 5 contributors

Commits on Apr 21, 2026

  1. feat(typed): add optional Pydantic namespace wrapper (#354)

    Adds ``g.typed`` — a lazy namespace accessor that wraps a curated set of
    high-value read endpoints with Pydantic response models, while leaving the
    132 existing methods entirely unchanged.
    
    Design follows Option D from the issue discussion:
    
        g = Garmin(email, password)
        raw = g.get_stats(date)           # dict[str, Any] — unchanged
        stats = g.typed.get_stats(date)   # DailyStats (Pydantic model)
    
    Scope of the first cut (7 methods, 6 response models):
    
        - get_stats / get_user_summary -> DailyStats
        - get_sleep_data               -> SleepData (with nested DailySleepDTO)
        - get_hrv_data                 -> HrvData | None
        - get_body_battery             -> list[BodyBatteryEntry]
        - get_training_readiness       -> list[TrainingReadiness]
        - get_activities_by_date       -> list[Activity]
    
    Design notes:
    
    - Lazy import. ``g.typed`` is a ``@functools.cached_property`` that imports
      ``garminconnect.typed`` on first access, so pydantic stays optional. If
      pydantic is missing, the import raises a clear install hint.
    - Kept in a new ``[typed]`` extra (pydantic>=2.0.0) rather than promoted to
      a core dep, so the default ``pip install garminconnect`` footprint doesn't
      change.
    - ``extra='allow'`` + all fields ``Optional`` — models tolerate Garmin schema
      drift (new firmware / subscription tiers add fields) and partial responses
      (privacy-protected accounts, missing device data). Unknown fields land in
      ``model_extra`` for callers who want them.
    - Validation failures raise ``GarminConnectResponseValidationError`` with the
      unvalidated response preserved on ``.raw`` so users can fall back without
      losing data.
    - ``get_training_readiness`` wrapper reflects the real list-shaped response
      (the existing ``dict[str, Any]`` annotation on the raw method is wrong but
      out of scope here).
    
    Marked **experimental** in module and class docstrings — model shapes may
    change between minor releases while the pattern stabilises.
    
    Tests (16 mocked, all green):
    
    - accessor returns cached ``TypedGarmin``
    - each wrapper calls through to the raw method and validates the response
    - extra fields tolerated (preserved in ``model_extra``)
    - missing fields default to ``None``
    - validation errors raise ``GarminConnectResponseValidationError`` with
      ``.raw`` populated
    - list endpoints with non-list responses return ``[]``
    - ``get_hrv_data`` ``None`` passes through unchanged
    
    Lint clean under ruff + mypy + black + isort.
    sorlen008 committed Apr 21, 2026
    Configuration menu
    Copy the full SHA
    ba2ad04 View commit details
    Browse the repository at this point in the history

Commits on Apr 26, 2026

  1. fix: route DI token endpoint to garmin.cn for CN accounts

    `is_cn=True` correctly routes the SSO and Connect API endpoints via the
    domain-aware `_sso` / `_connect` / `_connectapi` attributes, but the DI
    OAuth2 token endpoint stayed hardcoded to `diauth.garmin.com`. CN users
    don't exist in the .com user database, so:
    
    - Token exchange (post-SSO) succeeded for fresh logins because the SSO
      ticket validates server-side regardless of which DI auth host receives
      it (the .com endpoint accepts the ticket and issues a token whose JWT
      payload still carries `iss=https://diauth.garmin.cn` — confusing!)
    - BUT token refresh fails with 400:
      `{"error":"invalid_grant","error_description":"User doesn't exist in
      DB for GarminGUID: <uuid>"}`
      because the refresh path looks up the user by GUID in the host's DB.
    
    This matters in practice: the access token's TTL is ~4 hours, so any CN
    account that stays logged in longer than a single session will hit the
    refresh path and break.
    
    Fix: store `_di_token_url` as an instance attribute built from `domain`
    (same pattern as `_sso` / `_connect`), and use it in `_exchange_ticket()`
    and `_refresh_di_token()`. The module-level `DI_TOKEN_URL` constant is
    kept for backwards compatibility with external imports.
    
    Reproduce: `Garmin(email, password, is_cn=True).login()` then wait
    4+ hours, then make any API call → 401 → 400 invalid_grant on refresh.
    
    After fix: token refresh hits `https://diauth.garmin.cn/...` → 200.
    
    Co-Authored-By: Claude Opus 4.7 <[email protected]>
    MidnightV1 and claude committed Apr 26, 2026
    Configuration menu
    Copy the full SHA
    78565a4 View commit details
    Browse the repository at this point in the history

Commits on May 2, 2026

  1. Merge pull request #360 from MidnightV1/fix/cn-di-token-url

    fix: route DI token endpoint to garmin.cn for CN accounts
    cyberjunky authored May 2, 2026
    Configuration menu
    Copy the full SHA
    94ff883 View commit details
    Browse the repository at this point in the history
  2. Bumped version

    cyberjunky committed May 2, 2026
    Configuration menu
    Copy the full SHA
    64c76d6 View commit details
    Browse the repository at this point in the history
  3. Potential fix for pull request finding

    Co-authored-by: Copilot Autofix powered by AI <[email protected]>
    cyberjunky and Copilot authored May 2, 2026
    Configuration menu
    Copy the full SHA
    ff1db53 View commit details
    Browse the repository at this point in the history
  4. Merge pull request #359 from sorlen008/feat/typed-namespace-models

    feat(typed): add optional Pydantic namespace wrapper (#354)
    cyberjunky authored May 2, 2026
    Configuration menu
    Copy the full SHA
    be815ea View commit details
    Browse the repository at this point in the history
  5. fix: resolve CI failures — ruff format and mypy errors in tests

    - Auto-format typed.py (ruff format)
    - Add type: ignore[method-assign] to MagicMock assignments in test_typed.py
    - Pass response= as keyword arg to HTTPError constructor in test_retry_decorator.py
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    cyberjunky and claude committed May 2, 2026
    Configuration menu
    Copy the full SHA
    d206167 View commit details
    Browse the repository at this point in the history
Loading