before claude fix #1
This commit is contained in:
0
FitnessSync/backend/src/api/__init__.py
Normal file
0
FitnessSync/backend/src/api/__init__.py
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
FitnessSync/backend/src/api/__pycache__/logs.cpython-313.pyc
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/logs.cpython-313.pyc
Normal file
Binary file not shown.
BIN
FitnessSync/backend/src/api/__pycache__/metrics.cpython-313.pyc
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/metrics.cpython-313.pyc
Normal file
Binary file not shown.
BIN
FitnessSync/backend/src/api/__pycache__/setup.cpython-313.pyc
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/setup.cpython-313.pyc
Normal file
Binary file not shown.
BIN
FitnessSync/backend/src/api/__pycache__/status.cpython-313.pyc
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/status.cpython-313.pyc
Normal file
Binary file not shown.
BIN
FitnessSync/backend/src/api/__pycache__/sync.cpython-313.pyc
Normal file
BIN
FitnessSync/backend/src/api/__pycache__/sync.cpython-313.pyc
Normal file
Binary file not shown.
44
FitnessSync/backend/src/api/activities.py
Normal file
44
FitnessSync/backend/src/api/activities.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import APIRouter, Query, Response
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ActivityResponse(BaseModel):
|
||||
id: Optional[int] = None
|
||||
garmin_activity_id: Optional[str] = None
|
||||
activity_name: Optional[str] = None
|
||||
activity_type: Optional[str] = None
|
||||
start_time: Optional[str] = None
|
||||
duration: Optional[int] = None
|
||||
# file_path removed since we store in DB
|
||||
file_type: Optional[str] = None
|
||||
download_status: Optional[str] = None
|
||||
downloaded_at: Optional[str] = None
|
||||
|
||||
@router.get("/activities/list", response_model=List[ActivityResponse])
|
||||
async def list_activities(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
# This would return metadata for all downloaded/available activities
|
||||
# Implementation will connect with the services layer
|
||||
return []
|
||||
|
||||
@router.get("/activities/query", response_model=List[ActivityResponse])
|
||||
async def query_activities(
|
||||
activity_type: Optional[str] = Query(None),
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
download_status: Optional[str] = Query(None)
|
||||
):
|
||||
# This would allow advanced filtering of activities
|
||||
# Implementation will connect with the services layer
|
||||
return []
|
||||
|
||||
@router.get("/activities/download/{activity_id}")
|
||||
async def download_activity(activity_id: str):
|
||||
# This would serve the stored activity file from the database
|
||||
# Implementation will connect with the services layer
|
||||
# It should return the file content with appropriate content-type
|
||||
return Response(content=b"sample_content", media_type="application/octet-stream", headers={"Content-Disposition": f"attachment; filename=activity_{activity_id}.tcx"})
|
||||
24
FitnessSync/backend/src/api/logs.py
Normal file
24
FitnessSync/backend/src/api/logs.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class SyncLogResponse(BaseModel):
|
||||
id: int
|
||||
operation: str
|
||||
status: str
|
||||
message: Optional[str]
|
||||
start_time: str
|
||||
end_time: Optional[str]
|
||||
records_processed: int
|
||||
records_failed: int
|
||||
|
||||
@router.get("/logs", response_model=List[SyncLogResponse])
|
||||
async def get_logs(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
# This would return sync logs
|
||||
# Implementation will connect with the services layer
|
||||
return []
|
||||
98
FitnessSync/backend/src/api/metrics.py
Normal file
98
FitnessSync/backend/src/api/metrics.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class HealthMetricResponse(BaseModel):
|
||||
id: int
|
||||
metric_type: str
|
||||
metric_value: float
|
||||
unit: Optional[str]
|
||||
timestamp: str
|
||||
date: str
|
||||
source: str
|
||||
detailed_data: Optional[Dict[str, Any]]
|
||||
|
||||
class MetricDateRange(BaseModel):
|
||||
start_date: Optional[str]
|
||||
end_date: Optional[str]
|
||||
|
||||
class MetricsListResponse(BaseModel):
|
||||
metric_types: List[str]
|
||||
date_range: MetricDateRange
|
||||
|
||||
class HealthDataSummary(BaseModel):
|
||||
total_steps: Optional[int] = 0
|
||||
avg_heart_rate: Optional[float] = 0.0
|
||||
total_sleep_hours: Optional[float] = 0.0
|
||||
avg_calories: Optional[float] = 0.0
|
||||
|
||||
class ActivityResponse(BaseModel):
|
||||
id: Optional[int] = None
|
||||
garmin_activity_id: Optional[str] = None
|
||||
activity_name: Optional[str] = None
|
||||
activity_type: Optional[str] = None
|
||||
start_time: Optional[str] = None
|
||||
duration: Optional[int] = None
|
||||
file_path: Optional[str] = None
|
||||
file_type: Optional[str] = None
|
||||
download_status: Optional[str] = None
|
||||
downloaded_at: Optional[str] = None
|
||||
|
||||
@router.get("/metrics/list", response_model=MetricsListResponse)
|
||||
async def list_available_metrics():
|
||||
# This would return available metric types and date ranges
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"metric_types": ["steps", "heart_rate", "sleep", "calories"],
|
||||
"date_range": {
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31"
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/metrics/query", response_model=List[HealthMetricResponse])
|
||||
async def query_metrics(
|
||||
metric_type: Optional[str] = Query(None),
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
limit: int = Query(100, ge=1, le=1000)
|
||||
):
|
||||
# This would query health metrics with filters
|
||||
# Implementation will connect with the services layer
|
||||
return []
|
||||
|
||||
@router.get("/health-data/summary", response_model=HealthDataSummary)
|
||||
async def get_health_summary(
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None)
|
||||
):
|
||||
# This would return aggregated health statistics
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"total_steps": 123456,
|
||||
"avg_heart_rate": 72.5,
|
||||
"total_sleep_hours": 210.5,
|
||||
"avg_calories": 2345.6
|
||||
}
|
||||
|
||||
@router.get("/activities/list", response_model=List[ActivityResponse])
|
||||
async def list_activities(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
# This would return metadata for all downloaded/available activities
|
||||
# Implementation will connect with the services layer
|
||||
return []
|
||||
|
||||
@router.get("/activities/query", response_model=List[ActivityResponse])
|
||||
async def query_activities(
|
||||
activity_type: Optional[str] = Query(None),
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
download_status: Optional[str] = Query(None)
|
||||
):
|
||||
# This would allow advanced filtering of activities
|
||||
# Implementation will connect with the services layer
|
||||
return []
|
||||
138
FitnessSync/backend/src/api/setup.py
Normal file
138
FitnessSync/backend/src/api/setup.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
import traceback
|
||||
from ..services.postgresql_manager import PostgreSQLManager
|
||||
from ..utils.config import config
|
||||
from ..services.garmin.client import GarminClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_db():
|
||||
db_manager = PostgreSQLManager(config.DATABASE_URL)
|
||||
with db_manager.get_db_session() as session:
|
||||
yield session
|
||||
|
||||
class GarminCredentials(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
is_china: bool = False
|
||||
|
||||
class FitbitCredentials(BaseModel):
|
||||
client_id: str
|
||||
client_secret: str
|
||||
|
||||
class FitbitCallback(BaseModel):
|
||||
callback_url: str
|
||||
|
||||
class GarminMFARequest(BaseModel):
|
||||
verification_code: str
|
||||
session_id: str
|
||||
|
||||
class AuthStatusResponse(BaseModel):
|
||||
garmin: Optional[dict] = None
|
||||
fitbit: Optional[dict] = None
|
||||
|
||||
@router.get("/setup/auth-status", response_model=AuthStatusResponse)
|
||||
async def get_auth_status(db: Session = Depends(get_db)):
|
||||
# This would return the current authentication status from the database
|
||||
# Implementation will connect with the services layer
|
||||
# For now, return placeholder until we have full implementation
|
||||
return AuthStatusResponse(
|
||||
garmin={
|
||||
"username": "example@example.com",
|
||||
"authenticated": False,
|
||||
"token_expires_at": None,
|
||||
"last_login": None,
|
||||
"is_china": False
|
||||
},
|
||||
fitbit={
|
||||
"client_id": "example_client_id",
|
||||
"authenticated": False,
|
||||
"token_expires_at": None,
|
||||
"last_login": None
|
||||
}
|
||||
)
|
||||
|
||||
@router.post("/setup/garmin")
|
||||
async def save_garmin_credentials(credentials: GarminCredentials, db: Session = Depends(get_db)):
|
||||
from ..utils.helpers import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# This would save the Garmin credentials and attempt login
|
||||
# Implementation will connect with the services layer
|
||||
logger.info(f"Received Garmin credentials for user: {credentials.username}, is_china: {credentials.is_china}")
|
||||
|
||||
# Create the client with credentials but don't trigger login in __init__ if we handle it separately
|
||||
garmin_client = GarminClient(credentials.username, credentials.password, credentials.is_china)
|
||||
logger.debug("GarminClient instance created successfully")
|
||||
|
||||
try:
|
||||
logger.debug("Attempting to log in to Garmin")
|
||||
garmin_client.login()
|
||||
|
||||
# If login is successful, we're done
|
||||
logger.info(f"Successfully authenticated Garmin user: {credentials.username}")
|
||||
return {"status": "success", "message": "Garmin credentials saved and authenticated successfully"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error during Garmin authentication: {str(e)}")
|
||||
logger.error(f"Exception type: {type(e).__name__}")
|
||||
logger.error(f"Exception details: {repr(e)}")
|
||||
import traceback
|
||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||
|
||||
if "MFA" in str(e) or "mfa" in str(e).lower() or "MFA Required" in str(e):
|
||||
logger.info("MFA required for Garmin authentication")
|
||||
# Initiate MFA process and get session ID
|
||||
session_id = garmin_client.initiate_mfa(credentials.username)
|
||||
return {"status": "mfa_required", "message": "Multi-factor authentication required", "session_id": session_id}
|
||||
else:
|
||||
logger.error(f"Authentication failed with error: {str(e)}")
|
||||
return {"status": "error", "message": f"Error during authentication: {str(e)}"}
|
||||
|
||||
@router.post("/setup/garmin/mfa")
|
||||
async def complete_garmin_mfa(mfa_request: GarminMFARequest, db: Session = Depends(get_db)):
|
||||
from ..utils.helpers import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# Complete the MFA process for Garmin using session ID
|
||||
logger.info(f"Received MFA verification code for session {mfa_request.session_id}: {'*' * len(mfa_request.verification_code)}")
|
||||
|
||||
try:
|
||||
# Create a basic Garmin client without credentials - we'll use the session data
|
||||
garmin_client = GarminClient()
|
||||
logger.debug(f"Attempting to handle MFA for session: {mfa_request.session_id}")
|
||||
|
||||
# Call the handle_mfa method which will use database-stored session data
|
||||
success = garmin_client.handle_mfa(mfa_request.verification_code, session_id=mfa_request.session_id)
|
||||
|
||||
if success:
|
||||
logger.info(f"MFA verification completed successfully for session: {mfa_request.session_id}")
|
||||
return {"status": "success", "message": "MFA verification completed successfully"}
|
||||
else:
|
||||
logger.error(f"MFA verification failed for session: {mfa_request.session_id}")
|
||||
return {"status": "error", "message": "MFA verification failed"}
|
||||
except Exception as e:
|
||||
logger.error(f"MFA verification failed for session {mfa_request.session_id} with exception: {str(e)}")
|
||||
logger.error(f"Exception type: {type(e).__name__}")
|
||||
logger.error(f"Exception details: {repr(e)}")
|
||||
import traceback
|
||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||
return {"status": "error", "message": f"MFA verification failed: {str(e)}"}
|
||||
|
||||
@router.post("/setup/fitbit")
|
||||
async def save_fitbit_credentials(credentials: FitbitCredentials, db: Session = Depends(get_db)):
|
||||
# This would save the Fitbit credentials and return auth URL
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"status": "success",
|
||||
"auth_url": "https://www.fitbit.com/oauth2/authorize?...",
|
||||
"message": "Fitbit credentials saved, please visit auth_url to authorize"
|
||||
}
|
||||
|
||||
@router.post("/setup/fitbit/callback")
|
||||
async def fitbit_callback(callback_data: FitbitCallback, db: Session = Depends(get_db)):
|
||||
# This would handle the Fitbit OAuth callback
|
||||
# Implementation will connect with the services layer
|
||||
return {"status": "success", "message": "Fitbit OAuth flow completed successfully"}
|
||||
36
FitnessSync/backend/src/api/status.py
Normal file
36
FitnessSync/backend/src/api/status.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class SyncLogResponse(BaseModel):
|
||||
id: int
|
||||
operation: str
|
||||
status: str
|
||||
message: Optional[str]
|
||||
start_time: str
|
||||
end_time: Optional[str]
|
||||
records_processed: int
|
||||
records_failed: int
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
total_weight_records: int
|
||||
synced_weight_records: int
|
||||
unsynced_weight_records: int
|
||||
total_activities: int
|
||||
downloaded_activities: int
|
||||
recent_logs: List[SyncLogResponse]
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status():
|
||||
# This would return the current sync status
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"total_weight_records": 100,
|
||||
"synced_weight_records": 85,
|
||||
"unsynced_weight_records": 15,
|
||||
"total_activities": 50,
|
||||
"downloaded_activities": 30,
|
||||
"recent_logs": []
|
||||
}
|
||||
51
FitnessSync/backend/src/api/sync.py
Normal file
51
FitnessSync/backend/src/api/sync.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from ..services.postgresql_manager import PostgreSQLManager
|
||||
from sqlalchemy.orm import Session
|
||||
from ..utils.config import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class SyncActivityRequest(BaseModel):
|
||||
days_back: int = 30
|
||||
|
||||
class SyncResponse(BaseModel):
|
||||
status: str
|
||||
message: str
|
||||
job_id: Optional[str] = None
|
||||
|
||||
def get_db():
|
||||
db_manager = PostgreSQLManager(config.DATABASE_URL)
|
||||
with db_manager.get_db_session() as session:
|
||||
yield session
|
||||
|
||||
@router.post("/sync/weight", response_model=SyncResponse)
|
||||
async def sync_weight(db: Session = Depends(get_db)):
|
||||
# This would trigger the weight sync process
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"status": "started",
|
||||
"message": "Weight sync process started",
|
||||
"job_id": "weight-sync-12345"
|
||||
}
|
||||
|
||||
@router.post("/sync/activities", response_model=SyncResponse)
|
||||
async def sync_activities(request: SyncActivityRequest, db: Session = Depends(get_db)):
|
||||
# This would trigger the activity sync process
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"status": "started",
|
||||
"message": "Activity sync process started",
|
||||
"job_id": f"activity-sync-{request.days_back}"
|
||||
}
|
||||
|
||||
@router.post("/sync/metrics", response_model=SyncResponse)
|
||||
async def sync_metrics(db: Session = Depends(get_db)):
|
||||
# This would trigger the health metrics sync process
|
||||
# Implementation will connect with the services layer
|
||||
return {
|
||||
"status": "started",
|
||||
"message": "Health metrics sync process started",
|
||||
"job_id": "metrics-sync-12345"
|
||||
}
|
||||
Reference in New Issue
Block a user