Skip to content

Commit 1d60ea7

Browse files
committed
Rabbitcode fixes
1 parent e3d9d39 commit 1d60ea7

File tree

3 files changed

+53
-29
lines changed

3 files changed

+53
-29
lines changed

garminconnect/__init__.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
MAX_HYDRATION_ML = 10000 # 10 liters
2323
DATE_FORMAT_REGEX = r"^\d{4}-\d{2}-\d{2}$"
2424
DATE_FORMAT_STR = "%Y-%m-%d"
25-
TIMESTAMP_FORMAT_STR = "%Y-%m-%dT%H:%M:%S.%f"
2625
VALID_WEIGHT_UNITS = {"kg", "lbs"}
2726

2827

@@ -273,15 +272,18 @@ def connectapi(self, path: str, **kwargs: Any) -> Any:
273272
try:
274273
return self.garth.connectapi(path, **kwargs)
275274
except HTTPError as e:
276-
logger.error(f"API call failed for path '{path}': {e}")
277-
if e.response.status_code == 401:
275+
status = getattr(getattr(e, "response", None), "status_code", None)
276+
logger.error("API call failed for path '%s': %s (status=%s)", path, e, status)
277+
if status == 401:
278278
raise GarminConnectAuthenticationError(
279279
f"Authentication failed: {e}"
280280
) from e
281-
elif e.response.status_code == 429:
281+
elif status == 429:
282282
raise GarminConnectTooManyRequestsError(
283283
f"Rate limit exceeded: {e}"
284284
) from e
285+
else:
286+
raise GarminConnectConnectionError(f"HTTP error: {e}") from e
285287
except Exception as e:
286288
raise GarminConnectConnectionError(f"Connection error: {e}") from e
287289

@@ -290,7 +292,12 @@ def download(self, path: str, **kwargs: Any) -> Any:
290292
try:
291293
return self.garth.download(path, **kwargs)
292294
except Exception as e:
293-
logger.error(f"Download failed for path '{path}': {e}")
295+
status = getattr(getattr(e, "response", None), "status_code", None)
296+
logger.error("Download failed for path '%s': %s (status=%s)", path, e, status)
297+
if status == 401:
298+
raise GarminConnectAuthenticationError(f"Download error: {e}") from e
299+
if status == 429:
300+
raise GarminConnectTooManyRequestsError(f"Download error: {e}") from e
294301
raise GarminConnectConnectionError(f"Download error: {e}") from e
295302

296303
def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | None]:
@@ -366,7 +373,7 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
366373
if isinstance(e, GarminConnectAuthenticationError):
367374
raise
368375
else:
369-
logger.error("Login failed")
376+
logger.exception("Login failed")
370377
raise GarminConnectConnectionError(f"Login failed: {e}") from e
371378

372379
def resume_login(
@@ -623,6 +630,8 @@ def add_weigh_in_with_timestamps(
623630
else dt.astimezone(timezone.utc)
624631
)
625632

633+
# Validate weight for consistency with add_weigh_in
634+
weight = _validate_positive_number(weight, "weight")
626635
# Build the payload
627636
payload = {
628637
"dateTimestamp": dt.isoformat()[:19] + ".00", # Local time
@@ -641,6 +650,8 @@ def add_weigh_in_with_timestamps(
641650
def get_weigh_ins(self, startdate: str, enddate: str) -> dict[str, Any]:
642651
"""Get weigh-ins between startdate and enddate using format 'YYYY-MM-DD'."""
643652

653+
startdate = _validate_date_format(startdate, "startdate")
654+
enddate = _validate_date_format(enddate, "enddate")
644655
url = f"{self.garmin_connect_weight_url}/weight/range/{startdate}/{enddate}"
645656
params = {"includeAll": True}
646657
logger.debug("Requesting weigh-ins")
@@ -650,6 +661,7 @@ def get_weigh_ins(self, startdate: str, enddate: str) -> dict[str, Any]:
650661
def get_daily_weigh_ins(self, cdate: str) -> dict[str, Any]:
651662
"""Get weigh-ins for 'cdate' format 'YYYY-MM-DD'."""
652663

664+
cdate = _validate_date_format(cdate, "cdate")
653665
url = f"{self.garmin_connect_weight_url}/weight/dayview/{cdate}"
654666
params = {"includeAll": True}
655667
logger.debug("Requesting weigh-ins")
@@ -700,8 +712,11 @@ def get_body_battery(
700712
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
701713
"""
702714

715+
startdate = _validate_date_format(startdate, "startdate")
703716
if enddate is None:
704717
enddate = startdate
718+
else:
719+
enddate = _validate_date_format(enddate, "enddate")
705720
url = self.garmin_connect_daily_body_battery_url
706721
params = {"startDate": str(startdate), "endDate": str(enddate)}
707722
logger.debug("Requesting body battery data")
@@ -759,8 +774,11 @@ def get_blood_pressure(
759774
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
760775
"""
761776

777+
startdate = _validate_date_format(startdate, "startdate")
762778
if enddate is None:
763779
enddate = startdate
780+
else:
781+
enddate = _validate_date_format(enddate, "enddate")
764782
url = f"{self.garmin_connect_blood_pressure_endpoint}/{startdate}/{enddate}"
765783
params = {"includeAll": True}
766784
logger.debug("Requesting blood pressure data")
@@ -958,8 +976,7 @@ def add_hydration_data(
958976
}
959977

960978
logger.debug("Adding hydration data")
961-
962-
return self.garth.put("connectapi", url, json=payload)
979+
return self.garth.put("connectapi", url, json=payload).json()
963980

964981
def get_hydration_data(self, cdate: str) -> dict[str, Any]:
965982
"""Return available hydration data 'cdate' format 'YYYY-MM-DD'."""
@@ -1232,13 +1249,12 @@ def get_race_predictions(
12321249
return self.connectapi(url)
12331250

12341251
elif _type is not None and startdate is not None and enddate is not None:
1235-
url = (
1236-
self.garmin_connect_race_predictor_url + f"/{_type}/{self.display_name}"
1237-
)
1238-
params = {
1239-
"fromCalendarDate": str(startdate),
1240-
"toCalendarDate": str(enddate),
1241-
}
1252+
startdate = _validate_date_format(startdate, "startdate")
1253+
enddate = _validate_date_format(enddate, "enddate")
1254+
if (datetime.strptime(enddate, DATE_FORMAT_STR).date() - datetime.strptime(startdate, DATE_FORMAT_STR).date()).days > 366:
1255+
raise ValueError("Startdate cannot be more than one year before enddate")
1256+
url = self.garmin_connect_race_predictor_url + f"/{_type}/{self.display_name}"
1257+
params = {"fromCalendarDate": startdate, "toCalendarDate": enddate}
12421258
return self.connectapi(url, params=params)
12431259

12441260
else:
@@ -1247,6 +1263,7 @@ def get_race_predictions(
12471263
def get_training_status(self, cdate: str) -> dict[str, Any]:
12481264
"""Return training status data for current user."""
12491265

1266+
cdate = _validate_date_format(cdate, "cdate")
12501267
url = f"{self.garmin_connect_training_status_url}/{cdate}"
12511268
logger.debug("Requesting training status data")
12521269

@@ -1255,6 +1272,7 @@ def get_training_status(self, cdate: str) -> dict[str, Any]:
12551272
def get_fitnessage_data(self, cdate: str) -> dict[str, Any]:
12561273
"""Return Fitness Age data for current user."""
12571274

1275+
cdate = _validate_date_format(cdate, "cdate")
12581276
url = f"{self.garmin_connect_fitnessage}/{cdate}"
12591277
logger.debug("Requesting Fitness Age data")
12601278

@@ -1573,13 +1591,16 @@ def get_activities_by_date(
15731591
# 20 activities at a time
15741592
# and automatically loads more on scroll
15751593
url = self.garmin_connect_activities
1594+
startdate = _validate_date_format(startdate, "startdate")
1595+
if enddate is not None:
1596+
enddate = _validate_date_format(enddate, "enddate")
15761597
params = {
1577-
"startDate": str(startdate),
1598+
"startDate": startdate,
15781599
"start": str(start),
15791600
"limit": str(limit),
15801601
}
15811602
if enddate:
1582-
params["endDate"] = str(enddate)
1603+
params["endDate"] = enddate
15831604
if activitytype:
15841605
params["activityType"] = str(activitytype)
15851606
if sortorder:
@@ -1865,6 +1886,7 @@ def request_reload(self, cdate: str) -> dict[str, Any]:
18651886
Garmin offloads older data.
18661887
"""
18671888

1889+
cdate = _validate_date_format(cdate, "cdate")
18681890
url = f"{self.garmin_request_reload_url}/{cdate}"
18691891
logger.debug(f"Requesting reload of data for {cdate}.")
18701892

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ exclude_lines = [
146146
install = "pdm install --group :all"
147147
format = {composite = ["pdm run ruff check . --fix --unsafe-fixes", "pdm run isort . --skip-gitignore", "pdm run black -l 88 ."]}
148148
lint = {composite = ["pdm run isort --check-only . --skip-gitignore", "pdm run ruff check .", "pdm run black -l 88 . --check --diff", "pdm run mypy garminconnect tests"]}
149-
test = {env = {GARMINTOKENS = "~/.garminconnect"}, cmd = "pdm run coverage run -m pytest -v --durations=10"}
149+
test = {cmd = "pdm run coverage run -m pytest -v --durations=10"}
150150
testcov = {composite = ["test", "pdm run coverage html", "pdm run coverage xml -o coverage/coverage.xml"]}
151151
codespell = "pre-commit run codespell --all-files"
152152
clean = "python -c \"import shutil, pathlib; [shutil.rmtree(p, ignore_errors=True) for p in pathlib.Path('.').rglob('__pycache__')]; [p.unlink(missing_ok=True) for p in pathlib.Path('.').rglob('*.py[co]')]\""

tests/conftest.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
def vcr(vcr: Any) -> Any:
1111
# Set default GARMINTOKENS path if not already set
1212
if "GARMINTOKENS" not in os.environ:
13-
os.environ["GARMINTOKENS"] = "~/.garminconnect"
13+
os.environ["GARMINTOKENS"] = os.path.expanduser("~/.garminconnect")
1414
return vcr
1515

1616

@@ -20,13 +20,14 @@ def sanitize_cookie(cookie_value: str) -> str:
2020

2121
def scrub_dates(response: Any) -> Any:
2222
"""Scrub ISO datetime strings to make cassettes more stable."""
23-
body = response.get("body", {}).get("string")
23+
body_container = response.get("body") or {}
24+
body = body_container.get("string")
2425
if isinstance(body, str):
2526
# Replace ISO datetime strings with a fixed timestamp
2627
body = re.sub(
2728
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+", "1970-01-01T00:00:00.000", body
2829
)
29-
response["body"]["string"] = body
30+
body_container["string"] = body
3031
elif isinstance(body, bytes):
3132
# Handle bytes body
3233
body_str = body.decode("utf-8", errors="ignore")
@@ -35,7 +36,8 @@ def scrub_dates(response: Any) -> Any:
3536
"1970-01-01T00:00:00.000",
3637
body_str,
3738
)
38-
response["body"]["string"] = body_str.encode("utf-8")
39+
body_container["string"] = body_str.encode("utf-8")
40+
response["body"] = body_container
3941
return response
4042

4143

@@ -44,7 +46,7 @@ def sanitize_request(request: Any) -> Any:
4446
try:
4547
body = request.body.decode("utf8")
4648
except UnicodeDecodeError:
47-
...
49+
return request # leave as-is; binary bodies not sanitized
4850
else:
4951
for key in ["username", "password", "refresh_token"]:
5052
body = re.sub(key + r"=[^&]*", f"{key}=SANITIZED", body)
@@ -114,12 +116,12 @@ def sanitize_response(response: Any) -> Any:
114116

115117
body = json.dumps(body_json)
116118

117-
if isinstance(response["body"]["string"], bytes):
118-
response["body"]["string"] = body.encode("utf8")
119-
else:
120-
response["body"]["string"] = body
121-
122-
return response
119+
if "body" in response and "string" in response["body"]:
120+
if isinstance(response["body"]["string"], bytes):
121+
response["body"]["string"] = body.encode("utf8")
122+
else:
123+
response["body"]["string"] = body
124+
return response
123125

124126

125127
@pytest.fixture(scope="session")

0 commit comments

Comments
 (0)