@@ -43,7 +43,7 @@ def _validate_date_format(date_str: str, param_name: str = "date") -> str:
4343 # Validate that it's a real date
4444 datetime .strptime (date_str , DATE_FORMAT_STR )
4545 except ValueError as e :
46- raise ValueError (f"Invalid { param_name } : { e } " ) from e
46+ raise ValueError (f"invalid { param_name } : { e } " ) from e
4747
4848 return date_str
4949
@@ -134,7 +134,7 @@ def __init__(
134134 "/usersummary-service/usersummary/hydration/daily"
135135 )
136136 self .garmin_connect_set_hydration_url = (
137- "usersummary-service/usersummary/hydration/log"
137+ "/ usersummary-service/usersummary/hydration/log"
138138 )
139139 self .garmin_connect_daily_stats_steps_url = (
140140 "/usersummary-service/stats/steps/daily"
@@ -192,7 +192,7 @@ def __init__(
192192 "/periodichealth-service/menstrualcycle/dayview"
193193 )
194194 self .garmin_connect_pregnancy_snapshot_url = (
195- "periodichealth-service/menstrualcycle/pregnancysnapshot"
195+ "/ periodichealth-service/menstrualcycle/pregnancysnapshot"
196196 )
197197 self .garmin_connect_goals_url = "/goal-service/goal/goals"
198198
@@ -226,7 +226,6 @@ def __init__(
226226 self .garmin_connect_daily_intensity_minutes = (
227227 "/wellness-service/wellness/daily/im"
228228 )
229- self .garmin_all_day_stress_url = "/wellness-service/wellness/dailyStress"
230229 self .garmin_daily_events_url = "/wellness-service/wellness/dailyEvents"
231230 self .garmin_connect_activities = (
232231 "/activitylist-service/activities/search/activities"
@@ -350,8 +349,7 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
350349 self .password ,
351350 prompt_mfa = self .prompt_mfa ,
352351 )
353- # In MFA early-return mode, profile/settings are not loaded yet
354- return token1 , token2
352+ # Continue to load profile/settings below
355353
356354 # Validate profile data exists
357355 if not hasattr (self .garth , "profile" ) or not self .garth .profile :
@@ -544,7 +542,7 @@ def get_body_composition(
544542 datetime .strptime (startdate , DATE_FORMAT_STR ).date ()
545543 > datetime .strptime (enddate , DATE_FORMAT_STR ).date ()
546544 ):
547- raise ValueError ("Startdate cannot be after enddate" )
545+ raise ValueError ("startdate cannot be after enddate" )
548546 url = f"{ self .garmin_connect_weight_url } /weight/dateRange"
549547 params = {"startDate" : str (startdate ), "endDate" : str (enddate )}
550548 logger .debug ("Requesting body composition" )
@@ -612,7 +610,7 @@ def add_weigh_in(
612610 try :
613611 dt = datetime .fromisoformat (timestamp ) if timestamp else datetime .now ()
614612 except ValueError as e :
615- raise ValueError (f"Invalid timestamp format: { e } " ) from e
613+ raise ValueError (f"invalid timestamp format: { e } " ) from e
616614
617615 # Apply timezone offset to get UTC/GMT time
618616 dtGMT = dt .astimezone (timezone .utc )
@@ -639,7 +637,7 @@ def add_weigh_in_with_timestamps(
639637 url = f"{ self .garmin_connect_weight_url } /user-weight"
640638
641639 if unitKey not in VALID_WEIGHT_UNITS :
642- raise ValueError (f"UnitKey must be one of { VALID_WEIGHT_UNITS } " )
640+ raise ValueError (f"unitKey must be one of { VALID_WEIGHT_UNITS } " )
643641 # Validate and format the timestamps
644642 dt = datetime .fromisoformat (dateTimestamp ) if dateTimestamp else datetime .now ()
645643 dtGMT = (
@@ -895,7 +893,7 @@ def get_lactate_threshold(
895893 }
896894
897895 if start_date is None :
898- raise ValueError ("You must either specify 'latest=True' or a start_date" )
896+ raise ValueError ("you must either specify 'latest=True' or a start_date" )
899897
900898 if end_date is None :
901899 end_date = date .today ().isoformat ()
@@ -955,7 +953,7 @@ def add_hydration_data(
955953 raw_ts = datetime .strptime (cdate , "%Y-%m-%d" )
956954 timestamp = datetime .strftime (raw_ts , "%Y-%m-%dT%H:%M:%S.%f" )
957955 except ValueError as e :
958- raise ValueError (f"Invalid cdate: { e } " ) from e
956+ raise ValueError (f"invalid cdate: { e } " ) from e
959957
960958 elif cdate is None and timestamp is not None :
961959 # If timestamp is not null, validate and set cdate equal to date part of timestamp
@@ -983,7 +981,7 @@ def add_hydration_data(
983981 except ValueError as e :
984982 if "doesn't match" in str (e ):
985983 raise
986- raise ValueError (f"Invalid timestamp format: { e } " ) from e
984+ raise ValueError (f"invalid timestamp format: { e } " ) from e
987985
988986 payload = {
989987 "calendarDate" : cdate ,
@@ -1034,7 +1032,7 @@ def get_all_day_stress(self, cdate: str) -> dict[str, Any]:
10341032 """Return available all day stress data 'cdate' format 'YYYY-MM-DD'."""
10351033
10361034 cdate = _validate_date_format (cdate , "cdate" )
1037- url = f"{ self .garmin_all_day_stress_url } /{ cdate } "
1035+ url = f"{ self .garmin_connect_daily_stress_url } /{ cdate } "
10381036 logger .debug ("Requesting all day stress data" )
10391037
10401038 return self .connectapi (url )
@@ -1067,15 +1065,15 @@ def get_earned_badges(self) -> list[dict[str, Any]]:
10671065
10681066 return self .connectapi (url )
10691067
1070- def get_available_badges (self ) -> list [dict ]:
1068+ def get_available_badges (self ) -> list [dict [ str , Any ] ]:
10711069 """Return available badges for current user."""
10721070
10731071 url = self .garmin_connect_available_badges_url
10741072 logger .debug ("Requesting available badges for user" )
10751073
10761074 return self .connectapi (url , params = {"showExclusiveBadge" : "true" })
10771075
1078- def get_in_progress_badges (self ) -> list [dict ]:
1076+ def get_in_progress_badges (self ) -> list [dict [ str , Any ] ]:
10791077 """Return in progress badges for current user."""
10801078
10811079 logger .debug ("Requesting in progress badges for user" )
@@ -1283,7 +1281,7 @@ def get_race_predictions(
12831281 return self .connectapi (url , params = params )
12841282
12851283 else :
1286- raise ValueError ("You must either provide all parameters or no parameters" )
1284+ raise ValueError ("you must either provide all parameters or no parameters" )
12871285
12881286 def get_training_status (self , cdate : str ) -> dict [str , Any ]:
12891287 """Return training status data for current user."""
@@ -1546,12 +1544,12 @@ def upload_activity(self, activity_path: str) -> Any:
15461544
15471545 # Check if it's actually a file
15481546 if not p .is_file ():
1549- raise ValueError (f"Path is not a file: { activity_path } " )
1547+ raise ValueError (f"path is not a file: { activity_path } " )
15501548
15511549 file_base_name = p .name
15521550
15531551 if not file_base_name :
1554- raise ValueError ("Invalid file path - no filename found" )
1552+ raise ValueError ("invalid file path - no filename found" )
15551553
15561554 # More robust extension checking
15571555 file_parts = file_base_name .split ("." )
@@ -1787,7 +1785,7 @@ def download_activity(
17871785 Garmin .ActivityDownloadFormat .CSV : f"{ self .garmin_connect_csv_download } /{ activity_id } " , # noqa
17881786 }
17891787 if dl_fmt not in urls :
1790- raise ValueError (f"Unexpected value { dl_fmt } for dl_fmt" )
1788+ raise ValueError (f"unexpected value { dl_fmt } for dl_fmt" )
17911789 url = urls [dl_fmt ]
17921790
17931791 logger .debug ("Downloading activities from %s" , url )
@@ -1863,17 +1861,19 @@ def get_activity_details(
18631861
18641862 return self .connectapi (url , params = params )
18651863
1866- def get_activity_exercise_sets (self , activity_id : str ) -> dict [str , Any ]:
1864+ def get_activity_exercise_sets (self , activity_id : int | str ) -> dict [str , Any ]:
18671865 """Return activity exercise sets."""
18681866
1867+ activity_id = _validate_positive_integer (int (activity_id ), "activity_id" )
18691868 url = f"{ self .garmin_connect_activity } /{ activity_id } /exerciseSets"
18701869 logger .debug ("Requesting exercise sets for activity id %s" , activity_id )
18711870
18721871 return self .connectapi (url )
18731872
1874- def get_activity_gear (self , activity_id : str ) -> dict [str , Any ]:
1873+ def get_activity_gear (self , activity_id : int | str ) -> dict [str , Any ]:
18751874 """Return gears used for activity id."""
18761875
1876+ activity_id = _validate_positive_integer (int (activity_id ), "activity_id" )
18771877 params = {
18781878 "activityId" : str (activity_id ),
18791879 }
@@ -1882,7 +1882,9 @@ def get_activity_gear(self, activity_id: str) -> dict[str, Any]:
18821882
18831883 return self .connectapi (url , params = params )
18841884
1885- def get_gear_activities (self , gearUUID : str , limit : int = 9999 ) -> dict [str , Any ]:
1885+ def get_gear_activities (
1886+ self , gearUUID : str , limit : int = 9999
1887+ ) -> list [dict [str , Any ]]:
18861888 """Return activities where gear uuid was used.
18871889 :param gearUUID: UUID of the gear to get activities for
18881890 :param limit: Maximum number of activities to return (default: 9999)
@@ -1933,15 +1935,17 @@ def get_workouts(self, start: int = 0, limit: int = 100) -> dict[str, Any]:
19331935 params = {"start" : start , "limit" : limit }
19341936 return self .connectapi (url , params = params )
19351937
1936- def get_workout_by_id (self , workout_id : str ) -> dict [str , Any ]:
1938+ def get_workout_by_id (self , workout_id : int | str ) -> dict [str , Any ]:
19371939 """Return workout by id."""
19381940
1941+ workout_id = _validate_positive_integer (int (workout_id ), "workout_id" )
19391942 url = f"{ self .garmin_workouts } /workout/{ workout_id } "
19401943 return self .connectapi (url )
19411944
1942- def download_workout (self , workout_id : str ) -> bytes :
1945+ def download_workout (self , workout_id : int | str ) -> bytes :
19431946 """Download workout by id."""
19441947
1948+ workout_id = _validate_positive_integer (int (workout_id ), "workout_id" )
19451949 url = f"{ self .garmin_workouts } /workout/FIT/{ workout_id } "
19461950 logger .debug ("Downloading workout from %s" , url )
19471951
@@ -1961,9 +1965,11 @@ def upload_workout(
19611965 try :
19621966 payload = _json .loads (workout_json )
19631967 except Exception as e :
1964- raise ValueError (f"Invalid workout_json string: { e } " ) from e
1968+ raise ValueError (f"invalid workout_json string: { e } " ) from e
19651969 else :
19661970 payload = workout_json
1971+ if not isinstance (payload , dict | list ):
1972+ raise ValueError ("workout_json must be a JSON object or array" )
19671973 return self .garth .post ("connectapi" , url , json = payload , api = True ).json ()
19681974
19691975 def get_menstrual_data_for_date (self , fordate : str ) -> dict [str , Any ]:
@@ -1998,8 +2004,13 @@ def get_pregnancy_summary(self) -> dict[str, Any]:
19982004 return self .connectapi (url )
19992005
20002006 def query_garmin_graphql (self , query : dict [str , Any ]) -> dict [str , Any ]:
2001- """Returns the results of a POST request to the Garmin GraphQL Endpoints.
2002- Requires a GraphQL structured query. See {TBD} for examples.
2007+ """Execute a POST to Garmin's GraphQL endpoint.
2008+
2009+ Args:
2010+ query: A GraphQL request body, e.g. {"query": "...", "variables": {...}}
2011+ See example.py for example queries.
2012+ Returns:
2013+ Parsed JSON response as a dict.
20032014 """
20042015
20052016 logger .debug (f"Querying Garmin GraphQL Endpoint with query: { query } " )
0 commit comments