Giter Site home page Giter Site logo

garth's Introduction

Garth

CI codecov PyPI - Downloads

Garmin SSO auth + Connect Python client

Google Colabs

Stress levels from one day to another can vary by extremes, but there's always a general trend. Using a scatter plot with a rolling average shows both the individual days and the trend. The Colab retrieves up to three years of daily data. If there's less than three years of data, it retrieves whatever is available.

Stress: Garph of 28-day rolling average

The Garmin Connect app only shows a maximum of seven days for sleep stages—making it hard to see trends. The Connect API supports retrieving daily sleep quality in 28-day pages, but that doesn't show details. Using SleedData.list() gives us the ability to retrieve an arbitrary number of day with enough detail to product a stacked bar graph of the daily sleep stages.

Sleep stages over 90 days

One specific graph that's useful but not available in the Connect app is sleep start and end times over an extended period. This provides context to the sleep hours and stages.

Sleep times over 90 days

ChatGPT's Advanced Data Analysis took can provide incredible insight into the data in a way that's much simpler than using Pandas and Matplotlib.

Start by using the linked Colab to download a CSV of the last three years of your stats, and upload the CSV to ChatGPT.

Here's the outputs of the following prompts:

How do I sleep on different days of the week?

image

On what days do I exercise the most?

image

Magic!

Background

Garth is meant for personal use and follows the philosophy that your data is your data. You should be able to download it and analyze it in the way that you'd like. In my case, that means processing with Google Colab, Pandas, Matplotlib, etc.

There are already a few Garmin Connect libraries. Why write another?

Authentication and stability

The most important reasoning is to build a library with authentication that works on Google Colab and doesn't require tools like Cloudscraper. Garth, in comparison:

  1. Uses OAuth1 and OAuth2 token authentication after initial login
  2. OAuth1 token survives for a year
  3. Supports MFA
  4. Auto-refresh of OAuth2 token when expired
  5. Works on Google Colab
  6. Uses Pydantic dataclasses to validate and simplify use of data
  7. Full test coverage

JSON vs HTML

Using garth.connectapi() allows you to make requests to the Connect API and receive JSON vs needing to parse HTML. You can use the same endpoints the mobile app uses.

This also goes back to authentication. Garth manages the necessary Bearer Authentication (along with auto-refresh) necessary to make requests routed to the Connect API.

Instructions

Install

python -m pip install garth

Clone, setup environment and run tests

gh repo clone matin/garth
cd garth
make install
make

Use make help to see all the options.

Authenticate and save session

import garth
from getpass import getpass

email = input("Enter email address: ")
password = getpass("Enter password: ")
# If there's MFA, you'll be prompted during the login
garth.login(email, password)

garth.save("~/.garth")

Custom MFA handler

There's already a default MFA handler that prompts for the code in the terminal. You can provide your own handler. The handler should return the MFA code through your custom prompt.

garth.login(email, password, prompt_mfa=lambda: input("Enter MFA code: "))

Configure

Set domain for China

garth.configure(domain="garmin.cn")

Proxy through Charles

garth.configure(proxies={"https": "http://localhost:8888"}, ssl_verify=False)

Attempt to resume session

import garth
from garth.exc import GarthException

garth.resume("~/.garth")
try:
    garth.client.username
except GarthException:
    # Session is expired. You'll need to log in again

Connect API

Daily details

sleep = garth.connectapi(
    f"/wellness-service/wellness/dailySleepData/{garth.client.username}",
    params={"date": "2023-07-05", "nonSleepBufferMinutes": 60},
)
list(sleep.keys())
[
    "dailySleepDTO",
    "sleepMovement",
    "remSleepData",
    "sleepLevels",
    "sleepRestlessMoments",
    "restlessMomentsCount",
    "wellnessSpO2SleepSummaryDTO",
    "wellnessEpochSPO2DataDTOList",
    "wellnessEpochRespirationDataDTOList",
    "sleepStress"
]

Stats

stress =  garth.connectapi("/usersummary-service/stats/stress/weekly/2023-07-05/52")
{
    "calendarDate": "2023-07-13",
    "values": {
        "highStressDuration": 2880,
        "lowStressDuration": 10140,
        "overallStressLevel": 33,
        "restStressDuration": 30960,
        "mediumStressDuration": 8760
    }
}

Upload

with open("12129115726_ACTIVITY.fit", "rb") as f:
    uploaded = garth.client.upload(f)

Note: Garmin doesn't accept uploads of structured FIT files as outlined in this conversation. FIT files generated from workouts are accepted without issues.

{
    'detailedImportResult': {
        'uploadId': 212157427938,
        'uploadUuid': {
            'uuid': '6e56051d-1dd4-4f2c-b8ba-00a1a7d82eb3'
        },
        'owner': 2591602,
        'fileSize': 5289,
        'processingTime': 36,
        'creationDate': '2023-09-29 01:58:19.113 GMT',
        'ipAddress': None,
        'fileName': '12129115726_ACTIVITY.fit',
        'report': None,
        'successes': [],
        'failures': []
    }
}

Stats resources

Stress

Daily stress levels

DailyStress.list("2023-07-23", 2)
[
    DailyStress(
        calendar_date=datetime.date(2023, 7, 22),
        overall_stress_level=31,
        rest_stress_duration=31980,
        low_stress_duration=23820,
        medium_stress_duration=7440,
        high_stress_duration=1500
    ),
    DailyStress(
        calendar_date=datetime.date(2023, 7, 23),
        overall_stress_level=26,
        rest_stress_duration=38220,
        low_stress_duration=22500,
        medium_stress_duration=2520,
        high_stress_duration=300
    )
]

Weekly stress levels

WeeklyStress.list("2023-07-23", 2)
[
    WeeklyStress(calendar_date=datetime.date(2023, 7, 10), value=33),
    WeeklyStress(calendar_date=datetime.date(2023, 7, 17), value=32)
]

Steps

Daily steps

garth.DailySteps.list(period=2)
[
    DailySteps(
        calendar_date=datetime.date(2023, 7, 28),
        total_steps=6510,
        total_distance=5552,
        step_goal=8090
    ),
    DailySteps(
        calendar_date=datetime.date(2023, 7, 29),
        total_steps=7218,
        total_distance=6002,
        step_goal=7940
    )
]

Weekly steps

garth.WeeklySteps.list(period=2)
[
    WeeklySteps(
        calendar_date=datetime.date(2023, 7, 16),
        total_steps=42339,
        average_steps=6048.428571428572,
        average_distance=5039.285714285715,
        total_distance=35275.0,
        wellness_data_days_count=7
    ),
    WeeklySteps(
        calendar_date=datetime.date(2023, 7, 23),
        total_steps=56420,
        average_steps=8060.0,
        average_distance=7198.142857142857,
        total_distance=50387.0,
        wellness_data_days_count=7
    )
]

Intensity Minutes

Daily intensity minutes

garth.DailyIntensityMinutes.list(period=2)
[
    DailyIntensityMinutes(
        calendar_date=datetime.date(2023, 7, 28),
        weekly_goal=150,
        moderate_value=0,
        vigorous_value=0
    ),
    DailyIntensityMinutes(
        calendar_date=datetime.date(2023, 7, 29),
        weekly_goal=150,
        moderate_value=0,
        vigorous_value=0
    )
]

Weekly intensity minutes

garth.WeeklyIntensityMinutes.list(period=2)
[
    WeeklyIntensityMinutes(
        calendar_date=datetime.date(2023, 7, 17),
        weekly_goal=150,
        moderate_value=103,
        vigorous_value=9
    ),
    WeeklyIntensityMinutes(
        calendar_date=datetime.date(2023, 7, 24),
        weekly_goal=150,
        moderate_value=101,
        vigorous_value=105
    )
]

HRV

Daily HRV

garth.DailyHRV.list(period=2)
[
    DailyHRV(
        calendar_date=datetime.date(2023, 7, 28),
        weekly_avg=39,
        last_night_avg=36,
        last_night_5_min_high=52,
        baseline=HRVBaseline(
            low_upper=36,
            balanced_low=39,
            balanced_upper=51,
            marker_value=0.25
        ),
        status='BALANCED',
        feedback_phrase='HRV_BALANCED_2',
        create_time_stamp=datetime.datetime(2023, 7, 28, 12, 40, 16, 785000)
    ),
    DailyHRV(
        calendar_date=datetime.date(2023, 7, 29),
        weekly_avg=40,
        last_night_avg=41,
        last_night_5_min_high=76,
        baseline=HRVBaseline(
            low_upper=36,
            balanced_low=39,
            balanced_upper=51,
            marker_value=0.2916565
        ),
        status='BALANCED',
        feedback_phrase='HRV_BALANCED_8',
        create_time_stamp=datetime.datetime(2023, 7, 29, 13, 45, 23, 479000)
    )
]

Detailed HRV data

garth.HRVData.get("2023-07-20")
HRVData(
    user_profile_pk=2591602,
    hrv_summary=HRVSummary(
        calendar_date=datetime.date(2023, 7, 20),
        weekly_avg=39,
        last_night_avg=42,
        last_night_5_min_high=66,
        baseline=Baseline(
            low_upper=36,
            balanced_low=39,
            balanced_upper=52,
            marker_value=0.25
        ),
        status='BALANCED',
        feedback_phrase='HRV_BALANCED_7',
        create_time_stamp=datetime.datetime(2023, 7, 20, 12, 14, 11, 898000)
    ),
    hrv_readings=[
        HRVReading(
            hrv_value=54,
            reading_time_gmt=datetime.datetime(2023, 7, 20, 5, 29, 48),
            reading_time_local=datetime.datetime(2023, 7, 19, 23, 29, 48)
        ),
        HRVReading(
            hrv_value=56,
            reading_time_gmt=datetime.datetime(2023, 7, 20, 5, 34, 48),
            reading_time_local=datetime.datetime(2023, 7, 19, 23, 34, 48)
        ),
        # ... truncated for brevity
        HRVReading(
            hrv_value=38,
            reading_time_gmt=datetime.datetime(2023, 7, 20, 12, 9, 48),
            reading_time_local=datetime.datetime(2023, 7, 20, 6, 9, 48)
        )
    ],
    start_timestamp_gmt=datetime.datetime(2023, 7, 20, 5, 25),
    end_timestamp_gmt=datetime.datetime(2023, 7, 20, 12, 9, 48),
    start_timestamp_local=datetime.datetime(2023, 7, 19, 23, 25),
    end_timestamp_local=datetime.datetime(2023, 7, 20, 6, 9, 48),
    sleep_start_timestamp_gmt=datetime.datetime(2023, 7, 20, 5, 25),
    sleep_end_timestamp_gmt=datetime.datetime(2023, 7, 20, 12, 11),
    sleep_start_timestamp_local=datetime.datetime(2023, 7, 19, 23, 25),
    sleep_end_timestamp_local=datetime.datetime(2023, 7, 20, 6, 11)
)

Sleep

Daily sleep quality

garth.DailySleep.list("2023-07-23", 2)
[
    DailySleep(calendar_date=datetime.date(2023, 7, 22), value=69),
    DailySleep(calendar_date=datetime.date(2023, 7, 23), value=73)
]

Detailed sleep data

garth.SleepData.get("2023-07-20")
SleepData(
    daily_sleep_dto=DailySleepDTO(
        id=1689830700000,
        user_profile_pk=2591602,
        calendar_date=datetime.date(2023, 7, 20),
        sleep_time_seconds=23700,
        nap_time_seconds=0,
        sleep_window_confirmed=True,
        sleep_window_confirmation_type='enhanced_confirmed_final',
        sleep_start_timestamp_gmt=datetime.datetime(2023, 7, 20, 5, 25, tzinfo=TzInfo(UTC)),
        sleep_end_timestamp_gmt=datetime.datetime(2023, 7, 20, 12, 11, tzinfo=TzInfo(UTC)),
        sleep_start_timestamp_local=datetime.datetime(2023, 7, 19, 23, 25, tzinfo=TzInfo(UTC)),
        sleep_end_timestamp_local=datetime.datetime(2023, 7, 20, 6, 11, tzinfo=TzInfo(UTC)),
        unmeasurable_sleep_seconds=0,
        deep_sleep_seconds=9660,
        light_sleep_seconds=12600,
        rem_sleep_seconds=1440,
        awake_sleep_seconds=660,
        device_rem_capable=True,
        retro=False,
        sleep_from_device=True,
        sleep_version=2,
        awake_count=1,
        sleep_scores=SleepScores(
            total_duration=Score(
                qualifier_key='FAIR',
                optimal_start=28800.0,
                optimal_end=28800.0,
                value=None,
                ideal_start_in_seconds=None,
                deal_end_in_seconds=None
            ),
            stress=Score(
                qualifier_key='FAIR',
                optimal_start=0.0,
                optimal_end=15.0,
                value=None,
                ideal_start_in_seconds=None,
                ideal_end_in_seconds=None
            ),
            awake_count=Score(
                qualifier_key='GOOD',
                optimal_start=0.0,
                optimal_end=1.0,
                value=None,
                ideal_start_in_seconds=None,
                ideal_end_in_seconds=None
            ),
            overall=Score(
                qualifier_key='FAIR',
                optimal_start=None,
                optimal_end=None,
                value=68,
                ideal_start_in_seconds=None,
                ideal_end_in_seconds=None
            ),
            rem_percentage=Score(
                qualifier_key='POOR',
                optimal_start=21.0,
                optimal_end=31.0,
                value=6,
                ideal_start_in_seconds=4977.0,
                ideal_end_in_seconds=7347.0
            ),
            restlessness=Score(
                qualifier_key='EXCELLENT',
                optimal_start=0.0,
                optimal_end=5.0,
                value=None,
                ideal_start_in_seconds=None,
                ideal_end_in_seconds=None
            ),
            light_percentage=Score(
                qualifier_key='EXCELLENT',
                optimal_start=30.0,
                optimal_end=64.0,
                value=53,
                ideal_start_in_seconds=7110.0,
                ideal_end_in_seconds=15168.0
            ),
            deep_percentage=Score(
                qualifier_key='EXCELLENT',
                optimal_start=16.0,
                optimal_end=33.0,
                value=41,
                ideal_start_in_seconds=3792.0,
                ideal_end_in_seconds=7821.0
            )
        ),
        auto_sleep_start_timestamp_gmt=None,
        auto_sleep_end_timestamp_gmt=None,
        sleep_quality_type_pk=None,
        sleep_result_type_pk=None,
        average_sp_o2_value=92.0,
        lowest_sp_o2_value=87,
        highest_sp_o2_value=100,
        average_sp_o2_hr_sleep=53.0,
        average_respiration_value=14.0,
        lowest_respiration_value=12.0,
        highest_respiration_value=16.0,
        avg_sleep_stress=17.0,
        age_group='ADULT',
        sleep_score_feedback='NEGATIVE_NOT_ENOUGH_REM',
        sleep_score_insight='NONE'
    ),
    sleep_movement=[
        SleepMovement(
            start_gmt=datetime.datetime(2023, 7, 20, 4, 25),
            end_gmt=datetime.datetime(2023, 7, 20, 4, 26),
            activity_level=5.688743692980419
        ),
        SleepMovement(
            start_gmt=datetime.datetime(2023, 7, 20, 4, 26),
            end_gmt=datetime.datetime(2023, 7, 20, 4, 27),
            activity_level=5.318763075304898
        ),
        # ... truncated for brevity
        SleepMovement(
            start_gmt=datetime.datetime(2023, 7, 20, 13, 10),
            end_gmt=datetime.datetime(2023, 7, 20, 13, 11),
            activity_level=7.088729101943337
        )
    ]
)

List sleep data over several nights.

garth.SleepData.list("2023-07-20", 30)

User

UserProfile

garth.UserProfile.get()
UserProfile(
    id=3154645,
    profile_id=2591602,
    garmin_guid="0690cc1d-d23d-4412-b027-80fd4ed1c0f6",
    display_name="mtamizi",
    full_name="Matin Tamizi",
    user_name="mtamizi",
    profile_image_uuid="73240e81-6e4d-43fc-8af8-c8f6c51b3b8f",
    profile_image_url_large=(
        "https://s3.amazonaws.com/garmin-connect-prod/profile_images/"
        "73240e81-6e4d-43fc-8af8-c8f6c51b3b8f-2591602.png"
    ),
    profile_image_url_medium=(
        "https://s3.amazonaws.com/garmin-connect-prod/profile_images/"
        "685a19e9-a7be-4a11-9bf9-faca0c5d1f1a-2591602.png"
    ),
    profile_image_url_small=(
        "https://s3.amazonaws.com/garmin-connect-prod/profile_images/"
        "6302f021-0ec7-4dc9-b0c3-d5a19bc5a08c-2591602.png"
    ),
    location="Ciudad de México, CDMX",
    facebook_url=None,
    twitter_url=None,
    personal_website=None,
    motivation=None,
    bio=None,
    primary_activity=None,
    favorite_activity_types=[],
    running_training_speed=0.0,
    cycling_training_speed=0.0,
    favorite_cycling_activity_types=[],
    cycling_classification=None,
    cycling_max_avg_power=0.0,
    swimming_training_speed=0.0,
    profile_visibility="private",
    activity_start_visibility="private",
    activity_map_visibility="public",
    course_visibility="public",
    activity_heart_rate_visibility="public",
    activity_power_visibility="public",
    badge_visibility="private",
    show_age=False,
    show_weight=False,
    show_height=False,
    show_weight_class=False,
    show_age_range=False,
    show_gender=False,
    show_activity_class=False,
    show_vo_2_max=False,
    show_personal_records=False,
    show_last_12_months=False,
    show_lifetime_totals=False,
    show_upcoming_events=False,
    show_recent_favorites=False,
    show_recent_device=False,
    show_recent_gear=False,
    show_badges=True,
    other_activity=None,
    other_primary_activity=None,
    other_motivation=None,
    user_roles=[
        "SCOPE_ATP_READ",
        "SCOPE_ATP_WRITE",
        "SCOPE_COMMUNITY_COURSE_READ",
        "SCOPE_COMMUNITY_COURSE_WRITE",
        "SCOPE_CONNECT_READ",
        "SCOPE_CONNECT_WRITE",
        "SCOPE_DT_CLIENT_ANALYTICS_WRITE",
        "SCOPE_GARMINPAY_READ",
        "SCOPE_GARMINPAY_WRITE",
        "SCOPE_GCOFFER_READ",
        "SCOPE_GCOFFER_WRITE",
        "SCOPE_GHS_SAMD",
        "SCOPE_GHS_UPLOAD",
        "SCOPE_GOLF_API_READ",
        "SCOPE_GOLF_API_WRITE",
        "SCOPE_INSIGHTS_READ",
        "SCOPE_INSIGHTS_WRITE",
        "SCOPE_PRODUCT_SEARCH_READ",
        "ROLE_CONNECTUSER",
        "ROLE_FITNESS_USER",
        "ROLE_WELLNESS_USER",
        "ROLE_OUTDOOR_USER",
        "ROLE_CONNECT_2_USER",
        "ROLE_TACX_APP_USER",
    ],
    name_approved=True,
    user_profile_full_name="Matin Tamizi",
    make_golf_scorecards_private=True,
    allow_golf_live_scoring=False,
    allow_golf_scoring_by_connections=True,
    user_level=3,
    user_point=118,
    level_update_date="2020-12-12T15:20:38.0",
    level_is_viewed=False,
    level_point_threshold=140,
    user_point_offset=0,
    user_pro=False,
)

UserSettings

garth.UserSettings.get()
UserSettings(
    id=2591602,
    user_data=UserData(
        gender="MALE",
        weight=83000.0,
        height=182.0,
        time_format="time_twenty_four_hr",
        birth_date=datetime.date(1984, 10, 17),
        measurement_system="metric",
        activity_level=None,
        handedness="RIGHT",
        power_format=PowerFormat(
            format_id=30,
            format_key="watt",
            min_fraction=0,
            max_fraction=0,
            grouping_used=True,
            display_format=None,
        ),
        heart_rate_format=PowerFormat(
            format_id=21,
            format_key="bpm",
            min_fraction=0,
            max_fraction=0,
            grouping_used=False,
            display_format=None,
        ),
        first_day_of_week=FirstDayOfWeek(
            day_id=2,
            day_name="sunday",
            sort_order=2,
            is_possible_first_day=True,
        ),
        vo_2_max_running=45.0,
        vo_2_max_cycling=None,
        lactate_threshold_speed=0.34722125000000004,
        lactate_threshold_heart_rate=None,
        dive_number=None,
        intensity_minutes_calc_method="AUTO",
        moderate_intensity_minutes_hr_zone=3,
        vigorous_intensity_minutes_hr_zone=4,
        hydration_measurement_unit="milliliter",
        hydration_containers=[],
        hydration_auto_goal_enabled=True,
        firstbeat_max_stress_score=None,
        firstbeat_cycling_lt_timestamp=None,
        firstbeat_running_lt_timestamp=1044719868,
        threshold_heart_rate_auto_detected=True,
        ftp_auto_detected=None,
        training_status_paused_date=None,
        weather_location=None,
        golf_distance_unit="statute_us",
        golf_elevation_unit=None,
        golf_speed_unit=None,
        external_bottom_time=None,
    ),
    user_sleep=UserSleep(
        sleep_time=80400,
        default_sleep_time=False,
        wake_time=24000,
        default_wake_time=False,
    ),
    connect_date=None,
    source_type=None,
)

garth's People

Contributors

dependabot[bot] avatar dhireshjain avatar matin avatar nzigel avatar sdenel avatar yihong0618 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

garth's Issues

Issue connecting to API

Been mulling over this issue looking for a workaround for like 2 weeks now. Tried fresh install and multiple devices. I'm very new to programming so the issue could be painfully obvious.

Followed directions in the README and got up to the point of Connect API, at which point I input the sleep API request and get hit with an AssertionError.

Occurs on garth/http.py at line 127 with "assert self.oauth1_token".

Storing my login info and password seems to have worked. The oauth1 & ouath2 tokens also seem to be created.

I believe the tokens have been created because after inputting my username and password, a .garth folder is created in my user directory, and the JSON files pertaining to the tokens reside in there. Maybe that has something to do with it?

Screenshot 2024-01-11 at 2 47 26 AM

pydantic 1.10.12

First of all I wanted to express my appreciation for your work – your library is fantastic and has been incredibly useful.

I'm currently developing a custom integration for HomeAssistant, which relies on pydantic version 1.10.12 as part of its core dependencies. Would it be feasible for you to adjust the minimum required version of your library to be compatible with pydantic 1.10.12?

Add options to limit connection pool usage

Hi @matin there are a few users of my Garmin Connect home-assistant integration who have reported 'Connection pool is full errors' in their logs, I haven't run into them myself yet, but looking at other issues reported with same error in the Home Assistant repository a possible solution is to limit these from within the requests adapter.
Can you have a look if you can implement this in Garth?

Some references:
Ticket in my integration page
cyberjunky/home-assistant-garmin_connect#85

Similar issues and fixes in the code:
https://github.com/tuya/tuya-iot-python-sdk/pull/63/files
https://github.com/kbr/fritzconnection/pull/114/files

If you need any information, let me know, and thanks for your help in advance!

Give a hint in the documentation, howto install garth

On Debian Bookworm i could not install garth as described in the docu.

on Stackoverflow i found a solution.

Maybe we should give a hint in the documentation.

this steps solved it for me:

sudo apt install python3-pip
sudo mv /usr/lib/python3.11/EXTERNALLY-MANAGED /usr/lib/python3.11/EXTERNALLY-MANAGED.OFF
python3 -m pip install garth

Hydration get and set [feature request]

Hello @matin

Is there a way to add the capacity to get and set the hydration please ?
The Garmin connect app and the watch are not really ergonomic for this and that would allow an easy click a button integration to log a glass of water.
Thanks for the project.

Sleep example in documentation.... spelling error

Sleep example in documentation....
spelling error

sleep = garth.connectapi(
f"/wellness-service/wellness/dailySleepData/{garth.client.username}",
params={"date": "2023-07-05", "nonSleepBufferMinutes": 60),
)

maybe?

sleep = garth.connectapi(
f"/wellness-service/wellness/dailySleepData/{garth.client.username}",
params={"date": "2023-07-05", "nonSleepBufferMinutes": 60}
)

Async client?

Hi,
Nice work on this! I was considering using Garth for an interactive application. It would require making the http requests asynchronous. There are of course ways to achieve this at a higher level. But since this project is relatively small, and we have httpx now, I'd figured I'd get your thoughts on asyncio.
I'd of course be willing to contribute this. Browsing the code, it looks like it could be possible to abstract some stuff out of http.Client and provide 2 clients, one that is sync and one that is async. sso.py would be a little annoying to duplicate, I might have to look into how other libraries handle providing both.
Of course this is assuming you are interested in an async interface at all. I figure it's better than an entire different project or fork. What do you think?

when expired the refresh do not work

the issue comes about 1 day.
image

I printed the error text
its a html

<!doctype html>
<html lang="en">
  <head>
    <title>HTTP Status 401 – Unauthorized</title>
    <style type="text/css">
      body {
        font-family: Tahoma, Arial, sans-serif;
      }
      h1,
      h2,
      h3,
      b {
        color: white;
        background-color: #525d76;
      }
      h1 {
        font-size: 22px;
      }
      h2 {
        font-size: 16px;
      }
      h3 {
        font-size: 14px;
      }
      p {
        font-size: 12px;
      }
      a {
        color: bl ack;
      }
      .line {
        height: 1px;
        background-color: #525d76;
        border: none;
      }
    </style>
  </head>
  <body>
    <h1>HTTP Status 401 – Unauthorized</h1>
    <hr class="line" />
    <p><b>Type</b> Status Report</p>
    <p><b>Message</b> OAuthToken is invalid</p>
    <p>
      <b>Description</b> The request has not been applied to the target resource
      because it lacks valid authentication credentials for that resource.
    </p>
    <hr class="line" />
    <h3>Garmin Connect API Server</h3>
    <script
      defer
      src="https://static.cloudflareinsights.com/beacon.min.js/v8b253dfea2ab4077af8c6f58422dfbfd1689876627854"
      integrity="sha512-bjgnUKX4azu3dLTVtie9u6TKqgx29RBwfj3QXYt5EKfWM/9hPSAI/4qcV5NACjwAo8UtTeWefx6Zq5PHcMm7Tg=="
      data-cf-beacon='{"rayId":"8109fdc18c3a15a0","version":"2023.8.0","b":1,"token":"dfcba71ff1d44ca3956104d931b99217","si":100}'
      crossorigin="anonymous"
    ></script>
  </body>
</html>

Requirements and suggestions related to MFA handling

I want to implement -a much needed and requested- MFA functionality to my Home-Assistant integration home-assistant-garmin_connect

Garth handles this perfectly from cli/python-garminconnect's example.py code but for this to work from GUI code I think I need some small changes to or extra methods added to garth to handle MFA.

I think about this approach (by looking at the current code briefly due to time shortage) :

  • The garth login method (or a seperate new one) should not handle mfa by itself but it should just return a value if MFA is requested so python-garminconnect can detect the need to query the MFA code from the user
  • Followed by a call to a new (or changed) handle_mfa method where I specify the user supplied MFA code as argument

What do you think? If you have suggestions or a better approach (which you often have), please let me know.

Record VCR cassette of login from China

@yihong0618 I have a favor to ask ... Can you add a test (along with the VCR cassette) for login from China?

I spent a bunch of time to make sure it can work from China by allowing for a custom domain, but it hasn't been tested.

Using garth.configure(domain="garmin.cn") before the login should be sufficient 🤞

Let me know if you run into any issues (along with the error) for me to fix it. I want to make sure everyone is able to use Garth.

Upload does not support structured workout .fit-files - add that fact to docs

Thank you for writing and maintaining Garth!

I just wanted to let you know that - unsurprisingly - Garmin does not allow uploading structured workout files to the upload service. It seems to be only intended for Activities.

repro

I created a custom structured workout FIT file, exactly like the cookbook example from the FIT sdk, so a workout for a 10h endurance ride in zone 2, saved as test.FIT.
When I use

with open("test.FIT", "rb") as f:
    uploaded = garth.client.upload(f)

It confronts me with this error message:

Traceback (most recent call last):
  File "C:\Users\joskw\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\garth\http.py", line 127, in request
    self.last_resp.raise_for_status()
  File "C:\Users\joskw\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\requests\models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 406 Client Error: Not Acceptable for url: https://connectapi.garmin.com/upload-service/upload

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\Users\joskw\GitNoOneDrive\FIT-Garth\try-garth.py", line 33, in <module>
    uploaded = garth.client.upload(f)
  File "C:\Users\joskw\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\garth\http.py", line 173, in upload
    resp = self.post(
  File "C:\Users\joskw\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\garth\http.py", line 139, in post
    return self.request("POST", *args, **kwargs)
  File "C:\Users\joskw\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\garth\http.py", line 129, in request
    raise GarthHTTPError(
garth.exc.GarthHTTPError: Error in request: 406 Client Error: Not Acceptable for url: https://connectapi.garmin.com/upload-service/upload

I know there is no way for you to fix this. I just wanted to suggest that it would be helpful for others to mention this limitation in the documentation here: https://github.com/matin/garth#upload

Garmin Golf data

I'm not sure if this is the place to put this so I apologize in advance if this belongs somewhere else!

Q: Hi everyone, I was wondering if anyone knows how to access the garmin golf data through garth?

Previously I was using the garmin_golf repo to get golf data via json file. However as of a couple months ago it no longer works. From what I can tell it appears to be a change in the API (I could be way off) and I can't figure out how to make it work again.

Garth seems like the better option to access my garmin data so I was hoping someone could point me in the right direction for accessing the golf data. I've tried a few ways but I'm still very new to APIs so I'm not sure what exactly to put in the garth.connectapi() call.

TIA

Make awake_sleep_seconds optional and support None

I have some days in my Garmin Sleep data where I have no restless moments sleeping this results in an error:

daily_sleep_dto.awake_sleep_seconds
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]

Create docs with all known endpoints

While Garth will support some endpoints natively, it's impractical and slow to add all endpoints.

The faster method is to create a wiki with all known endpoints. The most popular can then be added directly into the library. Others can simply use garth.connectapi(path).

Garth doesn't work on Python 3.7

# python -m pip install garth
Collecting garth
  Could not find a version that satisfies the requirement garth (from versions: )
No matching distribution found for garth

garth.SleepData: DailySleepDTO missing 'sleep_from_device' and 'sleep_version'

When I do in provided colab notebook
garth.SleepData.get("2023-10-03")

I get error:

ValidationError Traceback (most recent call last)
in <cell line: 1>()
----> 1 garth.SleepData.get("2023-10-03")

2 frames
/usr/local/lib/python3.10/dist-packages/pydantic/dataclasses.cpython-310-x86_64-linux-gnu.so in pydantic.dataclasses._dataclass_validate_values()

ValidationError: 1 validation error for SleepData
daily_sleep_dto
DailySleepDTO.init() missing 2 required positional arguments: 'sleep_from_device' and 'sleep_version' (type=type_error)

"Update Phone Number" when signin

Thank you, @matin for your work on the this lib. I used your code as a basis for refactoring the garmin-connect which is a typescript lib for garmin connect Pythe1337N/garmin-connect#56

When the account has a phone number, the library works fine. However, if the account doesn't have a phone number, the POST request during the sign-in step returns an HTML page with the title "Update Phone Number", which causes the login to fail. If everything goes well, the title should be "Success".

Does anyone know how to add a phone number to a Garmin account? I tried adding a phone number on the MFA page, but I still can't see the phone number on the https://www.garmin.com/en-US/account/profile/ page.

https://www.garmin.com/en-US/account/profile/

Have phone numbers account:

image

Doesn't have phone numbers account:

image

Not working for China domain

Hi nice day!
Looks the lib not working for China domain anymore..
I have tested with 2 CN accounts, even tried reset the password.
All no luck TT

Build failing on MacOS with Python 3.12.1 at ... Install numpy 1.24.4 failed

MacOS Sonoma 14.2.1, using latest python 3.12.1

make install looks good all the way until the step for numpy 1.24.4, that fails and then pandas cannot install:

ERRORS:
add numpy failed:
Traceback (most recent call last):
File "/opt/homebrew/Cellar/[email protected]/3.12.1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/pdm/2.11.2/libexec/lib/python3.12/site-packages/pdm/installers/synchronizers.py", line 286, in install_candidate
self.manager.install(can)
File "/opt/homebrew/Cellar/pdm/2.11.2/libexec/lib/python3.12/site-packages/pdm/installers/manager.py", line 34, in install
dist_info = installer(str(prepared.build()), self.environment, prepared.direct_url())
^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/pdm/2.11.2/libexec/lib/python3.12/site-packages/pdm/models/candidates.py", line 418, in build
self.wheel = Path(builder.build(build_dir, metadata_directory=self._metadata_dir))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/pdm/2.11.2/libexec/lib/python3.12/site-packages/pdm/builders/wheel.py", line 26, in build
requires = self._hook.get_requires_for_build_wheel(config_settings)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pdm-install-qifi5at5.log

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.