Skip to content

Commit c079f7b

Browse files
committed
Normalize paths before passing to Garth
1 parent fa8844a commit c079f7b

File tree

1 file changed

+60
-5
lines changed

1 file changed

+60
-5
lines changed

garminconnect/__init__.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,20 @@ def connectapi(self, path: str, **kwargs: Any) -> Any:
295295
"""Wrapper for garth connectapi with error handling."""
296296
try:
297297
return self.garth.connectapi(path, **kwargs)
298+
except AssertionError as e:
299+
# Handle Windows-specific OAuth token refresh issue
300+
# This can occur when garth tries to refresh tokens during API calls
301+
error_msg = str(e).lower()
302+
if "oauth" in error_msg and (
303+
"oauth1" in error_msg or "oauth2" in error_msg
304+
):
305+
logger.error("OAuth token refresh failed during API call.")
306+
raise GarminConnectAuthenticationError(
307+
"Token refresh failed. Please re-authenticate. "
308+
f"Original error: {e}"
309+
) from e
310+
# Re-raise if it's a different AssertionError
311+
raise
298312
except (HTTPError, GarthHTTPError) as e:
299313
# For GarthHTTPError, extract status from the wrapped HTTPError
300314
if isinstance(e, GarthHTTPError):
@@ -369,12 +383,53 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
369383
token1 = None
370384
token2 = None
371385

386+
# Try to load tokens from tokenstore if provided
387+
tokens_loaded = False
372388
if tokenstore:
373-
if len(tokenstore) > 512:
374-
self.garth.loads(tokenstore)
375-
else:
376-
self.garth.load(tokenstore)
377-
else:
389+
try:
390+
if len(tokenstore) > 512:
391+
# Token data is provided directly as string (base64 encoded)
392+
self.garth.loads(tokenstore)
393+
else:
394+
# Tokenstore is a path - normalize it for cross-platform compatibility
395+
# This fixes Windows path issues where ~ expansion or path separators
396+
# might cause garth to not find all token files correctly
397+
tokenstore_path = Path(tokenstore).expanduser().resolve()
398+
# Convert to string with normalized path separators
399+
normalized_path = str(tokenstore_path)
400+
logger.debug(
401+
f"Loading tokens from normalized path: {normalized_path}"
402+
)
403+
self.garth.load(normalized_path)
404+
tokens_loaded = True
405+
except AssertionError as e:
406+
# Handle Windows-specific OAuth token refresh issue
407+
# When garth tries to refresh OAuth2 tokens, it may fail with:
408+
# "AssertionError: OAuth1 token is required for OAuth2 refresh"
409+
# This can occur if token files are incomplete or path resolution failed
410+
error_msg = str(e).lower()
411+
if "oauth" in error_msg and (
412+
"oauth1" in error_msg or "oauth2" in error_msg
413+
):
414+
logger.warning(
415+
"Token refresh failed (OAuth token mismatch). "
416+
"This may occur on Windows due to path or token file issues. "
417+
"Re-authentication required."
418+
)
419+
# Treat as invalid tokens - require re-authentication
420+
if not self.username or not self.password:
421+
raise GarminConnectAuthenticationError(
422+
"Stored tokens are invalid and credentials are required for re-authentication. "
423+
f"Original error: {e}"
424+
) from e
425+
# Will fall through to credential-based login below
426+
tokens_loaded = False
427+
else:
428+
# Re-raise if it's a different AssertionError
429+
raise
430+
431+
# If tokens weren't loaded (or failed to load), use credentials
432+
if not tokens_loaded:
378433
# Validate credentials before attempting login
379434
if not self.username or not self.password:
380435
raise GarminConnectAuthenticationError(

0 commit comments

Comments
 (0)