@@ -331,7 +331,7 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
331
331
)
332
332
333
333
# Validate email format when actually used for login
334
- if self .username and "@" not in self .username :
334
+ if not self . is_cn and self .username and "@" not in self .username :
335
335
raise GarminConnectAuthenticationError (
336
336
"Email must contain '@' symbol"
337
337
)
@@ -342,12 +342,16 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
342
342
self .password ,
343
343
return_on_mfa = self .return_on_mfa ,
344
344
)
345
+ # In MFA early-return mode, profile/settings are not loaded yet
346
+ return token1 , token2
345
347
else :
346
348
token1 , token2 = self .garth .login (
347
349
self .username ,
348
350
self .password ,
349
351
prompt_mfa = self .prompt_mfa ,
350
352
)
353
+ # In MFA early-return mode, profile/settings are not loaded yet
354
+ return token1 , token2
351
355
352
356
# Validate profile data exists
353
357
if not hasattr (self .garth , "profile" ) or not self .garth .profile :
@@ -532,8 +536,10 @@ def get_body_composition(
532
536
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'.
533
537
"""
534
538
535
- if enddate is None :
536
- enddate = startdate
539
+ startdate = _validate_date_format (startdate , "startdate" )
540
+ enddate = startdate if enddate is None else _validate_date_format (enddate , "enddate" )
541
+ if datetime .strptime (startdate , DATE_FORMAT_STR ).date () > datetime .strptime (enddate , DATE_FORMAT_STR ).date ():
542
+ raise ValueError ("Startdate cannot be after enddate" )
537
543
url = f"{ self .garmin_connect_weight_url } /weight/dateRange"
538
544
params = {"startDate" : str (startdate ), "endDate" : str (enddate )}
539
545
logger .debug ("Requesting body composition" )
@@ -556,6 +562,7 @@ def add_body_composition(
556
562
visceral_fat_rating : float | None = None ,
557
563
bmi : float | None = None ,
558
564
) -> dict [str , Any ]:
565
+ weight = _validate_positive_number (weight , "weight" )
559
566
dt = datetime .fromisoformat (timestamp ) if timestamp else datetime .now ()
560
567
fitEncoder = FitEncoderWeight ()
561
568
fitEncoder .write_file_info ()
@@ -626,6 +633,8 @@ def add_weigh_in_with_timestamps(
626
633
627
634
url = f"{ self .garmin_connect_weight_url } /user-weight"
628
635
636
+ if unitKey not in VALID_WEIGHT_UNITS :
637
+ raise ValueError (f"UnitKey must be one of { VALID_WEIGHT_UNITS } " )
629
638
# Validate and format the timestamps
630
639
dt = datetime .fromisoformat (dateTimestamp ) if dateTimestamp else datetime .now ()
631
640
dtGMT = (
@@ -858,25 +867,24 @@ def get_lactate_threshold(
858
867
# (or more, if cyclingHeartRate ever gets values) nearly identical dicts.
859
868
# We're combining them here
860
869
for entry in speed_and_heart_rate :
861
- if entry ["speed" ] is not None :
870
+ speed = entry .get ("speed" )
871
+ if speed is not None :
862
872
speed_and_heart_rate_dict ["userProfilePK" ] = entry ["userProfilePK" ]
863
873
speed_and_heart_rate_dict ["version" ] = entry ["version" ]
864
874
speed_and_heart_rate_dict ["calendarDate" ] = entry ["calendarDate" ]
865
875
speed_and_heart_rate_dict ["sequence" ] = entry ["sequence" ]
866
- speed_and_heart_rate_dict ["speed" ] = entry [ " speed" ]
876
+ speed_and_heart_rate_dict ["speed" ] = speed
867
877
868
878
# This is not a typo. The Garmin dictionary has a typo as of 2025-07-08, referring to it as "hearRate"
869
- elif entry [ "hearRate" ] is not None :
870
- speed_and_heart_rate_dict [ "heartRate" ] = entry [
871
- "hearRate"
872
- ] # Fix Garmin's typo
879
+ hr = entry . get ( "hearRate" )
880
+ if hr is not None :
881
+ speed_and_heart_rate_dict [ "heartRate" ] = hr
882
+ # Fix Garmin's typo
873
883
874
884
# Doesn't exist for me but adding it just in case. We'll check for each entry
875
- if entry ["heartRateCycling" ] is not None :
876
- speed_and_heart_rate_dict ["heartRateCycling" ] = entry [
877
- "heartRateCycling"
878
- ]
879
-
885
+ hrc = entry .get ("heartRateCycling" )
886
+ if hrc is not None :
887
+ speed_and_heart_rate_dict ["heartRateCycling" ] = hrc
880
888
return {
881
889
"speed_and_heart_rate" : speed_and_heart_rate_dict ,
882
890
"power" : power_dict ,
@@ -1205,6 +1213,7 @@ def get_endurance_score(
1205
1213
Using a range returns the aggregated weekly values for that week.
1206
1214
"""
1207
1215
1216
+ startdate = _validate_date_format (startdate , "startdate" )
1208
1217
if enddate is None :
1209
1218
url = self .garmin_connect_endurance_score_url
1210
1219
params = {"calendarDate" : str (startdate )}
@@ -1213,6 +1222,7 @@ def get_endurance_score(
1213
1222
return self .connectapi (url , params = params )
1214
1223
else :
1215
1224
url = f"{ self .garmin_connect_endurance_score_url } /stats"
1225
+ enddate = _validate_date_format (enddate , "enddate" )
1216
1226
params = {
1217
1227
"startDate" : str (startdate ),
1218
1228
"endDate" : str (enddate ),
@@ -1299,13 +1309,16 @@ def get_hill_score(
1299
1309
1300
1310
if enddate is None :
1301
1311
url = self .garmin_connect_hill_score_url
1312
+ startdate = _validate_date_format (startdate , "startdate" )
1302
1313
params = {"calendarDate" : str (startdate )}
1303
1314
logger .debug ("Requesting hill score data for a single day" )
1304
1315
1305
1316
return self .connectapi (url , params = params )
1306
1317
1307
1318
else :
1308
1319
url = f"{ self .garmin_connect_hill_score_url } /stats"
1320
+ startdate = _validate_date_format (startdate , "startdate" )
1321
+ enddate = _validate_date_format (enddate , "enddate" )
1309
1322
params = {
1310
1323
"startDate" : str (startdate ),
1311
1324
"endDate" : str (enddate ),
@@ -1351,6 +1364,8 @@ def get_device_solar_data(
1351
1364
else :
1352
1365
single_day = False
1353
1366
1367
+ startdate = _validate_date_format (startdate , "startdate" )
1368
+ enddate = _validate_date_format (enddate , "enddate" )
1354
1369
params = {"singleDayView" : single_day }
1355
1370
1356
1371
url = f"{ self .garmin_connect_solar_url } /{ device_id } /{ startdate } /{ enddate } "
@@ -1648,6 +1663,8 @@ def get_progress_summary_between_dates(
1648
1663
"""
1649
1664
1650
1665
url = self .garmin_connect_fitnessstats
1666
+ startdate = _validate_date_format (startdate , "startdate" )
1667
+ enddate = _validate_date_format (enddate , "enddate" )
1651
1668
params = {
1652
1669
"startDate" : str (startdate ),
1653
1670
"endDate" : str (enddate ),
@@ -1680,6 +1697,8 @@ def get_goals(
1680
1697
1681
1698
goals = []
1682
1699
url = self .garmin_connect_goals_url
1700
+ start = _validate_positive_integer (start , "start" )
1701
+ limit = _validate_positive_integer (limit , "limit" )
1683
1702
params = {
1684
1703
"status" : status ,
1685
1704
"start" : str (start ),
@@ -1832,10 +1851,9 @@ def get_activity_details(
1832
1851
"""Return activity details."""
1833
1852
1834
1853
activity_id = str (activity_id )
1835
- params = {
1836
- "maxChartSize" : str (maxchart ),
1837
- "maxPolylineSize" : str (maxpoly ),
1838
- }
1854
+ maxchart = _validate_positive_integer (maxchart , "maxchart" )
1855
+ maxpoly = _validate_positive_integer (maxpoly , "maxpoly" )
1856
+ params = {"maxChartSize" : str (maxchart ), "maxPolylineSize" : str (maxpoly )}
1839
1857
url = f"{ self .garmin_connect_activity } /{ activity_id } /details"
1840
1858
logger .debug ("Requesting details for activity id %s" , activity_id )
1841
1859
@@ -1844,7 +1862,7 @@ def get_activity_details(
1844
1862
def get_activity_exercise_sets (self , activity_id : str ) -> dict [str , Any ]:
1845
1863
"""Return activity exercise sets."""
1846
1864
1847
- activity_id = str (activity_id )
1865
+ activity_id = _validate_positive_integer (activity_id , "activity_id" )
1848
1866
url = f"{ self .garmin_connect_activity } /{ activity_id } /exerciseSets"
1849
1867
logger .debug ("Requesting exercise sets for activity id %s" , activity_id )
1850
1868
@@ -1853,7 +1871,7 @@ def get_activity_exercise_sets(self, activity_id: str) -> dict[str, Any]:
1853
1871
def get_activity_gear (self , activity_id : str ) -> dict [str , Any ]:
1854
1872
"""Return gears used for activity id."""
1855
1873
1856
- activity_id = str (activity_id )
1874
+ activity_id = _validate_positive_integer (activity_id , "activity_id" )
1857
1875
params = {
1858
1876
"activityId" : str (activity_id ),
1859
1877
}
@@ -1907,19 +1925,23 @@ def get_workouts(self, start: int = 0, limit: int = 100) -> dict[str, Any]:
1907
1925
"""Return workouts from start till end."""
1908
1926
1909
1927
url = f"{ self .garmin_workouts } /workouts"
1928
+ start = _validate_non_negative_integer (start , "start" )
1929
+ limit = _validate_positive_integer (limit , "limit" )
1910
1930
logger .debug (f"Requesting workouts from { start } with limit { limit } " )
1911
1931
params = {"start" : start , "limit" : limit }
1912
1932
return self .connectapi (url , params = params )
1913
1933
1914
1934
def get_workout_by_id (self , workout_id : str ) -> dict [str , Any ]:
1915
1935
"""Return workout by id."""
1916
1936
1937
+ workout_id = _validate_positive_integer (workout_id , "workout_id" )
1917
1938
url = f"{ self .garmin_workouts } /workout/{ workout_id } "
1918
1939
return self .connectapi (url )
1919
1940
1920
1941
def download_workout (self , workout_id : str ) -> bytes :
1921
1942
"""Download workout by id."""
1922
1943
1944
+ workout_id = _validate_positive_integer (workout_id , "workout_id" )
1923
1945
url = f"{ self .garmin_workouts } /workout/FIT/{ workout_id } "
1924
1946
logger .debug ("Downloading workout from %s" , url )
1925
1947
@@ -1958,6 +1980,8 @@ def get_menstrual_calendar_data(
1958
1980
) -> dict [str , Any ]:
1959
1981
"""Return summaries of cycles that have days between startdate and enddate."""
1960
1982
1983
+ startdate = _validate_date_format (startdate , "startdate" )
1984
+ enddate = _validate_date_format (enddate , "enddate" )
1961
1985
url = f"{ self .garmin_connect_menstrual_calendar_url } /{ startdate } /{ enddate } "
1962
1986
logger .debug (
1963
1987
f"Requesting menstrual data for dates { startdate } through { enddate } "
0 commit comments