restrcuted repo

This commit is contained in:
2025-09-26 06:52:27 -07:00
parent 997028f0e1
commit 15974bbac5
26 changed files with 17 additions and 12 deletions

0
cli/__init__.py Normal file
View File

274
cli/cli_interface.py Normal file
View File

@@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
CLI Interface - Simple command line interface for the cycling analyzer
"""
import asyncio
import logging
from pathlib import Path
import argparse
import sys
from rich.console import Console
from rich.markdown import Markdown
from ..config import Config, load_config, create_sample_config
from ..core.core_app import CyclingAnalyzerApp
from ..core.template_engine import create_default_templates
class CLI:
"""Command line interface"""
def __init__(self):
self.app = None
async def run(self, args):
"""Main CLI loop"""
# Setup configuration
try:
config = self._setup_config()
self.app = CyclingAnalyzerApp(config, test_mode=args.test)
# Setup logging
logging.basicConfig(level=getattr(logging, config.log_level.upper()))
# Initialize app
await self.app.initialize()
if args.command:
await self._handle_command(args)
else:
# Show initial status
await self._show_status()
# Main loop
await self._main_loop()
except KeyboardInterrupt:
print("\nGoodbye!")
except Exception as e:
print(f"Error: {e}")
logging.error(f"CLI error: {e}", exc_info=True)
finally:
if self.app:
await self.app.cleanup()
def _setup_config(self) -> Config:
"""Setup configuration and default files"""
# Create sample config if needed
create_sample_config()
# Load config
config = load_config()
# Validate required settings
if not config.openrouter_api_key or config.openrouter_api_key == "your_openrouter_api_key_here":
print("Please edit config.yaml with your OpenRouter API key")
print("Get your key from: https://openrouter.ai")
raise ValueError("OpenRouter API key not configured")
# Create default templates
create_default_templates(config.templates_dir)
return config
async def _show_status(self):
"""Show application status"""
print(f"\nStatus:")
print(f"- Available tools: {len(await self.app.list_available_tools())}")
print(f"- Available templates: {len(self.app.list_templates())}")
print(f"- Cached data keys: {list(self.app.get_cached_data().keys())}")
async def _main_loop(self):
"""Main interaction loop"""
while True:
print(f"\n{'='*60}")
print("CYCLING WORKOUT ANALYZER")
print(f"{'='*60}")
print("1. Analyze last cycling workout")
print("2. Get next workout suggestion")
print("3. Enhanced analysis")
print("4. List available MCP tools")
print("5. List available templates")
print("6. Show cached data")
print("7. Clear cache")
print("8. Exit")
print("-" * 60)
choice = input("Enter your choice (1-8): ").strip()
try:
if choice == "1":
await self._analyze_last_workout()
elif choice == "2":
await self._suggest_next_workout()
elif choice == "3":
await self._enhanced_analysis()
elif choice == "4":
await self._list_tools()
elif choice == "5":
self._list_templates()
elif choice == "6":
self._show_cached_data()
elif choice == "7":
self._clear_cache()
elif choice == "8":
break
else:
print("Invalid choice. Please try again.")
except Exception as e:
print(f"Error: {e}")
logging.error(f"Menu action error: {e}")
input("\nPress Enter to continue...")
async def _analyze_last_workout(self):
"""Analyze last workout"""
print("\nAnalyzing your last workout...")
# Load training rules
rules = self._load_training_rules()
result = await self.app.analyze_workout(
analysis_type="analyze_last_workout",
training_rules=rules
)
print("\n" + "="*50)
print("WORKOUT ANALYSIS")
print("="*50)
console = Console()
console.print(Markdown(result))
async def _suggest_next_workout(self):
"""Suggest next workout"""
print("\nGenerating workout suggestion...")
# Load training rules
rules = self._load_training_rules()
result = await self.app.suggest_next_workout(training_rules=rules)
print("\n" + "="*50)
print("NEXT WORKOUT SUGGESTION")
print("="*50)
console = Console()
console.print(Markdown(result))
async def _enhanced_analysis(self):
"""Enhanced analysis menu"""
print("\nSelect analysis type:")
print("a) Performance trends")
print("b) Training load analysis")
print("c) Recovery assessment")
choice = input("Enter choice (a-c): ").strip().lower()
analysis_types = {
'a': 'performance trends',
'b': 'training load',
'c': 'recovery assessment'
}
if choice not in analysis_types:
print("Invalid choice.")
return
analysis_type = analysis_types[choice]
print(f"\nPerforming {analysis_type} analysis...")
# Load training rules
rules = self._load_training_rules()
result = await self.app.enhanced_analysis(
analysis_type,
training_rules=rules
)
print(f"\n{'='*50}")
print(f"ENHANCED {analysis_type.upper()} ANALYSIS")
print("="*50)
console = Console()
console.print(Markdown(result))
async def _list_tools(self):
"""List available tools"""
tools = await self.app.list_available_tools()
if tools:
self.app.mcp_client.print_tools()
else:
print("No MCP tools available")
def _list_templates(self):
"""List available templates"""
templates = self.app.list_templates()
print(f"\nAvailable templates ({len(templates)}):")
for template in templates:
print(f" - {template}")
def _show_cached_data(self):
"""Show cached data"""
cached_data = self.app.get_cached_data()
print(f"\nCached data ({len(cached_data)} items):")
for key, value in cached_data.items():
data_type = type(value).__name__
if isinstance(value, (dict, list)):
size = len(value)
print(f" - {key}: {data_type} (size: {size})")
else:
print(f" - {key}: {data_type}")
def _clear_cache(self):
"""Clear cache"""
self.app.cache_manager.clear()
print("Cache cleared")
def _load_training_rules(self) -> str:
"""Load training rules from file"""
rules_file = Path(self.app.config.rules_file)
if rules_file.exists():
with open(rules_file, 'r') as f:
return f.read()
else:
# Create default rules
default_rules = """
Training Goals:
- Improve FTP (Functional Threshold Power)
- Build endurance for long rides
- Maintain consistent training
Power Zones (adjust based on your FTP):
- Zone 1 (Active Recovery): < 55% FTP
- Zone 2 (Endurance): 56-75% FTP
- Zone 3 (Tempo): 76-90% FTP
- Zone 4 (Lactate Threshold): 91-105% FTP
- Zone 5 (VO2 Max): 106-120% FTP
Weekly Structure:
- 70-80% easy/moderate intensity
- 20-30% high intensity
- At least 1 rest day per week
"""
rules_file.parent.mkdir(exist_ok=True)
with open(rules_file, 'w') as f:
f.write(default_rules)
return default_rules
async def _handle_command(self, args):
"""Handle non-interactive commands"""
if args.command == "analyze_last":
await self._analyze_last_workout()
else:
print(f"Unknown command: {args.command}")
async def main():
"""CLI entry point"""
parser = argparse.ArgumentParser(description="Cycling Workout Analyzer")
parser.add_argument('command', nargs='?', help="Command to execute (e.g., analyze_last)")
parser.add_argument('--test', action='store_true', help="Test mode: print rendered prompt without calling LLM")
args = parser.parse_args()
cli = CLI()
await cli.run(args)
if __name__ == "__main__":
asyncio.run(main())

285
cli/setup_garth.py Normal file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Garth Setup Script
Helper script to set up Garth authentication and test connection
"""
import os
import json
import sys
from pathlib import Path
try:
import garth
print("✅ Garth module available")
except ImportError:
print("❌ Garth module not found")
print("Install with: pip install garth")
sys.exit(1)
def setup_garth_auth():
"""Setup Garth authentication"""
print("🔐 Setting up Garth authentication...")
print("This will guide you through authenticating with Garmin Connect")
print()
# Check if session already exists
session_path = Path.home() / ".garth"
if session_path.exists():
print("📁 Existing Garth session found")
choice = input("Do you want to use existing session? (y/n): ").strip().lower()
if choice == 'y':
try:
garth.resume(str(session_path))
# Test session
test_result = test_garth_connection()
if test_result:
print("✅ Existing session is valid")
return True
else:
print("❌ Existing session is invalid, creating new one...")
except Exception as e:
print(f"⚠️ Could not resume session: {e}")
print("Creating new session...")
# Create new session
print("\n🆕 Creating new Garth session...")
print("You'll need your Garmin Connect credentials")
print()
try:
# Configure Garth
garth.configure()
# Get credentials
username = input("Garmin Connect username/email: ").strip()
password = input("Garmin Connect password: ").strip()
if not username or not password:
print("❌ Username and password are required")
return False
print("\n🔄 Logging into Garmin Connect...")
# Login
garth.login(username, password)
# Save session
garth.save(str(session_path))
print(f"💾 Session saved to {session_path}")
# Test connection
if test_garth_connection():
print("✅ Authentication successful!")
return True
else:
print("❌ Authentication failed")
return False
except Exception as e:
print(f"❌ Authentication error: {e}")
return False
def test_garth_connection():
"""Test Garth connection"""
print("🧪 Testing Garth connection...")
# Use the endpoint we know works
try:
print(" Trying social profile endpoint...")
user_info = garth.connectapi("/userprofile-service/socialProfile")
if user_info and isinstance(user_info, dict):
display_name = user_info.get('displayName', 'Unknown')
full_name = user_info.get('fullName', 'Unknown')
print(f"✅ Connected as: {display_name} ({full_name})")
# Test activities too
try:
activities = garth.connectapi("/activitylist-service/activities/search/activities", params={"limit": 1})
if activities and len(activities) > 0:
print(f"✅ Activities accessible: Found {len(activities)} recent activity")
else:
print(f"⚠️ Activities accessible but none found")
except Exception as e:
print(f"⚠️ Activities test failed: {str(e)[:50]}")
return True
else:
print("❌ Invalid response from social profile endpoint")
return False
except Exception as e:
print(f"❌ Connection test failed: {e}")
return False
def extract_token_info():
"""Extract token information for config"""
print("\n🎫 Extracting token information...")
session_path = Path.home() / ".garth"
if not session_path.exists():
print("❌ No Garth session found. Run authentication first.")
return None
try:
# Load session data
with open(session_path, 'r') as f:
session_data = json.load(f)
# Extract token (this might need adjustment based on Garth's session format)
token = session_data.get('oauth_token') or session_data.get('token')
if token:
print(f"✅ Token extracted: {token[:20]}...")
print("\n📝 Add this to your config.yaml:")
print(f"garth_token: \"{token}\"")
return token
else:
print("❌ Could not extract token from session")
print("Available session keys:", list(session_data.keys()))
return None
except Exception as e:
print(f"❌ Error extracting token: {e}")
return None
def show_user_info():
"""Show current user information"""
print("\n👤 User Information:")
# Try multiple endpoints to get user info
endpoints_to_try = [
("/userprofile-service/socialProfile", "Social Profile"),
("/user-service/users/settings", "User Settings"),
("/modern/currentuser-service/user/profile", "User Profile"),
("/userprofile-service/userprofile", "User Profile Alt"),
]
user_info = None
working_endpoint = None
for endpoint, name in endpoints_to_try:
try:
print(f" Trying {name}...")
data = garth.connectapi(endpoint)
if data and isinstance(data, dict):
user_info = data
working_endpoint = endpoint
print(f"{name} successful")
break
except Exception as e:
print(f"{name} failed: {str(e)[:100]}")
continue
if user_info:
print(f"\n Working Endpoint: {working_endpoint}")
print(f" Data keys available: {list(user_info.keys())}")
# Extract common fields
display_name = (
user_info.get('displayName') or
user_info.get('fullName') or
user_info.get('userName') or
user_info.get('profileDisplayName') or
'Unknown'
)
user_id = (
user_info.get('id') or
user_info.get('userId') or
user_info.get('profileId') or
'Unknown'
)
print(f" Display Name: {display_name}")
print(f" User ID: {user_id}")
# Show first few fields for debugging
print(f" Sample data:")
for key, value in list(user_info.items())[:5]:
print(f" {key}: {str(value)[:50]}")
# Try to get activities
try:
print(f"\n Testing activities access...")
activities = garth.connectapi("/activitylist-service/activities/search/activities",
params={"limit": 1})
if activities and len(activities) > 0:
print(f" Recent Activities: ✅ Available ({len(activities)} found)")
activity = activities[0]
activity_type = activity.get('activityType', {})
if isinstance(activity_type, dict):
type_name = activity_type.get('typeKey', 'Unknown')
else:
type_name = str(activity_type)
print(f" Last Activity: {type_name}")
else:
print(f" Recent Activities: ⚠️ None found")
except Exception as e:
print(f" Recent Activities: ❌ Error ({str(e)[:50]})")
else:
print(" ❌ Could not retrieve any user information")
# Show what endpoints are available
print("\n Available endpoints check:")
test_endpoints = [
"/userstats-service/statistics",
"/wellness-service/wellness",
"/device-service/deviceRegistration",
]
for endpoint in test_endpoints:
try:
garth.connectapi(endpoint)
print(f"{endpoint} - Available")
except Exception as e:
if "404" in str(e):
print(f"{endpoint} - Not Found")
elif "403" in str(e):
print(f" ⚠️ {endpoint} - Forbidden (auth issue)")
else:
print(f"{endpoint} - Error: {str(e)[:30]}")
def main():
"""Main setup function"""
print("Garth Setup and Authentication")
print("=" * 40)
while True:
print("\nOptions:")
print("1. Setup/Login to Garmin Connect")
print("2. Test existing connection")
print("3. Show user information")
print("4. Extract token for config")
print("5. Exit")
choice = input("\nEnter choice (1-5): ").strip()
if choice == "1":
success = setup_garth_auth()
if success:
show_user_info()
elif choice == "2":
test_garth_connection()
elif choice == "3":
show_user_info()
elif choice == "4":
token = extract_token_info()
if token:
# Also show how to set environment variable
print(f"\nOr set as environment variable:")
print(f"export GARTH_TOKEN=\"{token}\"")
elif choice == "5":
print("👋 Goodbye!")
break
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
main()