working
This commit is contained in:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user