import os
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
from dotenv import load_dotenv
def get_upcoming_appointments(api_key):
"""Fetches upcoming appointments from the Cal.com API."""
print("Retrieving upcoming appointments...")
url = f"https://api.cal.com/v1/bookings?apiKey={api_key}&status=upcoming"
try:
response = requests.get(url)
response.raise_for_status()
bookings = response.json().get("bookings", [])
# Ensure we only process appointments that are not cancelled and sort them.
accepted_bookings = sorted(
[b for b in bookings if b.get("status") == "ACCEPTED"],
key=lambda b: b['startTime']
)
return accepted_bookings
except requests.exceptions.RequestException as e:
print(f"Error fetching appointments: {e}")
return []
def get_weather_for_location(location):
"""Gets current weather for a location using a free weather API."""
try:
# Using wttr.in - a free weather service that returns JSON
url = f"https://wttr.in/{location}?format=j1"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
if 'current_condition' in data and data['current_condition']:
current = data['current_condition'][0]
return {
'temperature_f': current.get('temp_F', 'Unknown'),
'temperature_c': current.get('temp_C', 'Unknown'),
'condition': current.get('weatherDesc', [{}])[0].get('value', 'Unknown'),
'humidity': current.get('humidity', 'Unknown'),
'wind_mph': current.get('windspeedMiles', 'Unknown'),
'wind_dir': current.get('winddir16Point', 'Unknown')
}
except Exception as e:
print(f"Error fetching weather: {e}")
return None
def extract_location_from_booking(booking):
"""
Enhanced location extraction with multiple fallback strategies.
Based on Cal.com webhook documentation analysis via MCP Context7.
"""
# Strategy 1: Check top-level location field (most reliable)
location = booking.get('location')
if location and isinstance(location, str) and location.strip():
if not location.startswith('integrations:') and 'http' not in location:
print(f"✅ Found location in top-level field: {location}")
return location.strip()
# Strategy 2: Check responses.location structure (legacy format)
responses = booking.get('responses', {})
if isinstance(responses, dict):
location_response = responses.get('location', {})
if isinstance(location_response, dict):
# Handle nested value structure
location_value = location_response.get('value')
if isinstance(location_value, dict):
location = location_value.get('value')
elif isinstance(location_value, str):
location = location_value
if location and isinstance(location, str) and location.strip():
if not location.startswith('integrations:') and 'http' not in location:
print(f"✅ Found location in responses field: {location}")
return location.strip()
# Strategy 3: Extract from event title or description
title = booking.get('title', '')
if 'at ' in title.lower():
potential_location = title.lower().split('at ')[-1].strip()
if len(potential_location) > 2 and len(potential_location) < 50:
print(f"✅ Extracted location from title: {potential_location}")
return potential_location
return None
def display_summary(appointment, weather, location):
"""Displays a summary of the appointment and weather forecast."""
# Convert UTC time string from API to a local datetime object for display
utc_start_time = datetime.fromisoformat(appointment['startTime'].replace('Z', '+00:00'))
local_tz = ZoneInfo(appointment.get("user", {}).get("timeZone") or "America/New_York")
local_start_time = utc_start_time.astimezone(local_tz)
print("\n" + "="*60)
print("🗓️ APPOINTMENT & WEATHER SUMMARY")
print("="*60)
print(f"📋 Meeting: {appointment['title']}")
print(f"🕐 When: {local_start_time.strftime('%A, %B %d, %Y, at %I:%M %p %Z')}")
print(f"📍 Where: {location}")
if weather:
print(f"\n🌤️ WEATHER FORECAST FOR {location.upper()}:")
print(f" 🌡️ Temperature: {weather['temperature_f']}°F ({weather['temperature_c']}°C)")
print(f" ☁️ Conditions: {weather['condition']}")
print(f" 💨 Wind: {weather['wind_mph']} mph {weather['wind_dir']}")
print(f" 💧 Humidity: {weather['humidity']}%")
else:
print(f"\n❌ Could not retrieve weather forecast for {location}")
print("="*60)
def main():
"""Main function to run the script."""
load_dotenv()
cal_api_key = os.getenv("CALCOM_API_KEY")
if not cal_api_key:
print("Error: CALCOM_API_KEY not found. Please check your .env file.")
return
appointments = get_upcoming_appointments(cal_api_key)
if not appointments:
print("No upcoming appointments found.")
return
print(f"\n📅 Found {len(appointments)} upcoming appointment(s):")
print("-" * 50)
for i, appt in enumerate(appointments):
# Convert UTC time for display in the menu
utc_start = datetime.fromisoformat(appt['startTime'].replace('Z', '+00:00'))
local_tz = ZoneInfo(appt.get("user", {}).get("timeZone") or "America/New_York")
local_start = utc_start.astimezone(local_tz)
print(f" {i+1}: {appt['title']} on {local_start.strftime('%a, %b %d at %I:%M %p %Z')}")
try:
choice = int(input(f"\nSelect appointment (1-{len(appointments)}): ")) - 1
if not 0 <= choice < len(appointments):
raise ValueError
except ValueError:
print("❌ Invalid selection.")
return
selected_appointment = appointments[choice]
# Use enhanced location extraction
location = extract_location_from_booking(selected_appointment)
if not location:
print("\n❌ No physical location found for this appointment.")
print("💡 Weather lookup requires a city name or address in the appointment location.")
return
print(f"\n🔍 Looking up weather for: {location}")
weather = get_weather_for_location(location)
display_summary(selected_appointment, weather, location)
if __name__ == "__main__":
main()