#!/usr/bin/env python3 import os from datetime import datetime, timedelta import logging import gradio as gr from whoop import WhoopClient # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Global Whoop client whoop_client = None def initialize_whoop_client_with_input(email, password): """ Authenticates a user using the provided WHOOP email and password. Stores credentials in the environment variables and initializes the WhoopClient. Args: email (str): WHOOP account email. password (str): WHOOP account password. Returns: str: Success or error message indicating authentication status. """ global whoop_client if not email or not password: return "❌ Please enter both email and password." os.environ["WHOOP_EMAIL"] = email os.environ["WHOOP_PASSWORD"] = password try: whoop_client = WhoopClient(username=email, password=password) return "✅ Successfully authenticated with Whoop." except Exception as e: return f"❌ Authentication failed: {e}" def get_latest_cycle_gr(): """ Retrieves the most recent WHOOP cycle (recovery data) for the authenticated user. Returns: str: Summary of latest recovery data or an error message. """ if not whoop_client: return "❌ Not authenticated." try: end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") cycles = whoop_client.get_cycle_collection(start_date, end_date) if not cycles: return "⚠️ No cycle data available." latest = cycles[0] score = latest.get("score", {}).get("recovery") recovery_score = f"{score}" if score is not None else "Not Available" return f"🌀 Latest Cycle:\nRecovery Score: {recovery_score}\n\nFull Data:\n{latest}" except Exception as e: return f"❌ Error: {e}" def get_average_strain_gr(days): """ Calculates the average strain over a given number of past days. Args: days (int): Number of days to include in the average. Returns: str: Average strain value or an error message. """ if not whoop_client: return "❌ Not authenticated." try: end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=int(days))).strftime("%Y-%m-%d") cycles = whoop_client.get_cycle_collection(start_date, end_date) if not cycles: return "⚠️ No cycle data available." strains = [c['score']['strain'] for c in cycles if c.get('score') and c['score'].get('strain') is not None] if not strains: return "⚠️ No strain data available." avg = sum(strains) / len(strains) return f"📊 Average Strain over {days} days: {avg:.2f} (from {len(strains)} entries)" except Exception as e: return f"❌ Error: {e}" def format_latest_cycle(raw_text): """ Formats the raw text returned by get_latest_cycle_gr for display. Args: raw_text (str): Raw output from get_latest_cycle_gr. Returns: Tuple[str, str]: A short summary and detailed cycle data. """ if raw_text.startswith("❌") or raw_text.startswith("⚠️"): return raw_text, "" try: lines = raw_text.splitlines() recovery_score_line = next((line for line in lines if "Recovery Score" in line), "Recovery Score: Not Available") full_data = "\n".join(lines[2:]) # skip title and recovery line return recovery_score_line, full_data except Exception as e: return f"❌ Failed to parse cycle: {e}", "" # UI definition with gr.Blocks(title="Whoop API Explorer") as demo: gr.Markdown("# 🧠 WHOOP API Dashboard") gr.Markdown("Easily authenticate and explore your WHOOP recovery and strain data.") with gr.Group(): gr.Markdown("## 🔐 Authentication") with gr.Row(): email_input = gr.Textbox(label="WHOOP Email", placeholder="your@email.com") password_input = gr.Textbox(label="WHOOP Password", type="password", placeholder="••••••••") auth_button = gr.Button("Authenticate") auth_output = gr.Label(label="Auth Status") with gr.Group(): gr.Markdown("## 🔁 Latest Recovery Cycle") cycle_button = gr.Button("Fetch Latest Cycle") latest_recovery = gr.Label(label="Recovery Score") cycle_details = gr.Textbox(label="Full Cycle Data", visible=False, lines=6) with gr.Group(): gr.Markdown("## 📊 Strain Insights") with gr.Row(): days_input = gr.Number(value=7, label="Number of Days", precision=0) strain_button = gr.Button("Calculate Average Strain") average_strain = gr.Label(label="Average Strain") # Bind actions auth_button.click(fn=initialize_whoop_client_with_input, inputs=[email_input, password_input], outputs=auth_output) cycle_button.click(fn=lambda: format_latest_cycle(get_latest_cycle_gr()), outputs=[latest_recovery, cycle_details]) strain_button.click(fn=get_average_strain_gr, inputs=days_input, outputs=average_strain) # Launch app if __name__ == "__main__": demo.launch(mcp_server=True)