This commit is contained in:
2026-01-01 07:14:18 -08:00
parent 25745cf6d6
commit c45e41b6a9
100 changed files with 8068 additions and 2424 deletions

View File

@@ -1,66 +1,144 @@
import fitbit
from fitbit import exceptions
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import logging
import time
from ..utils.helpers import setup_logger
logger = setup_logger(__name__)
class FitbitClient:
def __init__(self, client_id: str, client_secret: str, access_token: str = None, refresh_token: str = None):
def __init__(self, client_id: str, client_secret: str, access_token: str = None, refresh_token: str = None, redirect_uri: str = None):
self.client_id = client_id
self.client_secret = client_secret
self.access_token = access_token
self.refresh_token = refresh_token
self.fitbit_client = None
self.redirect_uri = redirect_uri
self.fitbit = None
if access_token and refresh_token:
self.fitbit_client = fitbit.Fitbit(
client_id=client_id,
client_secret=client_secret,
# Initialize Fitbit class if we have enough info, or just for auth flow
# The example initializes it immediately
if client_id and client_secret:
self.fitbit = fitbit.Fitbit(
client_id,
client_secret,
access_token=access_token,
refresh_token=refresh_token,
# Callback for token refresh if needed
redirect_uri=redirect_uri,
timeout=10
)
def get_authorization_url(self, redirect_uri: str) -> str:
def get_authorization_url(self, redirect_uri: str = None) -> str:
"""Generate authorization URL for Fitbit OAuth flow."""
# This would generate the Fitbit authorization URL
auth_url = f"https://www.fitbit.com/oauth2/authorize?response_type=code&client_id={self.client_id}&redirect_uri={redirect_uri}&scope=weight"
# Update internal redirect_uri if provided
if redirect_uri:
self.redirect_uri = redirect_uri
# Re-init or update client? Fitbit class uses it in init.
# It seems simpler to recreate the instance or update the client manually if supported.
# But based on the example, we can just init with it.
self.fitbit = fitbit.Fitbit(
self.client_id,
self.client_secret,
redirect_uri=redirect_uri,
timeout=10
)
# The example calls self.fitbit.client.authorize_token_url()
# Note: The fitbit library might default to certain scopes or we need to pass them?
# The example used: url, _ = self.fitbit.client.authorize_token_url()
# But we need scopes.
scope = ['weight', 'nutrition', 'activity', 'sleep', 'heartrate', 'profile']
# The underlying client is oauthlib.oauth2.WebApplicationClient usually
auth_url, _ = self.fitbit.client.authorize_token_url(scope=scope)
logger.info(f"Generated Fitbit authorization URL: {auth_url}")
return auth_url
def exchange_code_for_token(self, code: str, redirect_uri: str) -> Dict[str, str]:
def exchange_code_for_token(self, code: str, redirect_uri: str = None) -> Dict[str, Any]:
"""Exchange authorization code for access and refresh tokens."""
# This would exchange the authorization code for tokens
# Implementation would use the Fitbit library to exchange the code
# If redirect_uri is provided here, ensure we are using a client configured with it
if redirect_uri and redirect_uri != self.redirect_uri:
self.fitbit = fitbit.Fitbit(
self.client_id,
self.client_secret,
redirect_uri=redirect_uri,
timeout=10
)
logger.info(f"Exchanging authorization code for tokens")
# Return mock response for now
return {
"access_token": "mock_access_token",
"refresh_token": "mock_refresh_token",
"expires_at": (datetime.now() + timedelta(hours=1)).isoformat()
}
# The example: self.fitbit.client.fetch_access_token(code)
# It updates the internal token automatically.
token = self.fitbit.client.fetch_access_token(code)
return token
def get_weight_logs(self, start_date: str, end_date: str = None) -> List[Dict[str, Any]]:
"""Fetch weight logs from Fitbit API."""
if not self.fitbit_client:
if not self.fitbit:
raise Exception("Fitbit client not authenticated")
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
try:
# Get weight logs from Fitbit
weight_logs = self.fitbit_client.get_bodyweight(
print(f"Making request to Fitbit API: get_bodyweight(base_date={start_date}, end_date={end_date})", flush=True)
# get_bodyweight returns {'weight': [...]}
weight_logs = self.fitbit.get_bodyweight(
base_date=start_date,
end_date=end_date
)
logger.info(f"Fetched {len(weight_logs.get('weight', []))} weight entries from Fitbit")
return weight_logs.get('weight', [])
logs = weight_logs.get('weight', [])
print(f"Fitbit Response: Success. Fetched {len(logs)} weight entries.", flush=True)
return logs
except exceptions.HTTPTooManyRequests as e:
retry_after = e.retry_after_secs if hasattr(e, 'retry_after_secs') else 'unknown'
print(f"Fitbit API Rate Limit Hit! Retry-After: {retry_after} seconds.", flush=True)
if not retry_after or retry_after == 'unknown':
if hasattr(e, 'response') and e.response is not None:
retry_after = e.response.headers.get('Retry-After', 'unknown')
print(f"Rate Limited. Recommended wait: {retry_after}", flush=True)
raise e
except KeyError as e:
# Handle specific KeyError from fitbit library when parsing 429 response
if str(e).strip("'\"") == 'retry-after':
print("Fitbit Library KeyError 'retry-after' detected. This is a Rate Limit event.", flush=True)
# Raise as TooManyRequests manually
# We don't have the response object easily here unless we dig into stack,
# so we assume a safe default wait time.
print("Rate Limited (inferred). Recommended wait: 60 seconds (default).", flush=True)
# Create a mock exception or just raise HTTPTooManyRequests with limited info
# The library's exception requires a response object in init usually, but let's try to simulate or just raise generic
# Actually, better to raise the library's exception if possible, or our own wrappers.
# Let's just re-raise as the libraries exception but monkey-patch the retry_after_secs if possible?
# No, just raise it and let the caller handle it.
# Since we can't easily construct the proper exception without a response object,
# we'll raise a new HTTPTooManyRequests with a valid retry_after_secs if we can, or just let sync.py handle generic error?
# Sync.py catches Exception.
# Better: Log clearly and raise a clean error that implies rate limit so user sees it.
# AND if we want to auto-retry, we need to signal that.
# Let's assume 60s wait.
# We can construct a dummy object to hold the retry value if needed,
# but for now let's just print and raise.
# To make sync.py sleep, we can modify sync.py.
# Or here, we can sleep? No, sync.py controls the loop.
pass
# Fall through to generic Exception handler which prints it?
# No, we want to customize the message.
raise Exception("Fitbit Rate Limit Hit (Library Error). Retry-After: 60s") from e
raise e
except Exception as e:
logger.error(f"Error fetching weight logs from Fitbit: {str(e)}")
print(f"Error fetching weight logs from Fitbit: {str(e)}", flush=True)
if hasattr(e, 'response') and e.response is not None:
print(f"Fitbit API Error Response: {e.response.text}", flush=True)
print(f"Fitbit API Error Headers: {e.response.headers}", flush=True)
raise e
def refresh_access_token(self) -> Dict[str, str]: