"""
Google Calendar Client
Handles interaction with Google Calendar API to add course deadlines.
Google Calendar API Documentation: https://developers.google.com/calendar/api/v3/reference
"""
import os
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
try:
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
GOOGLE_API_AVAILABLE = True
except ImportError:
GOOGLE_API_AVAILABLE = False
print("⚠️ Google API libraries not installed. Install with: pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client")
# Scopes required for Google Calendar access
SCOPES = ['https://www.googleapis.com/auth/calendar']
class GoogleCalendarClient:
"""Client for interacting with Google Calendar API"""
def __init__(self, credentials_path: str = "credentials.json", token_path: str = "token.json"):
"""
Initialize Google Calendar client.
Args:
credentials_path: Path to Google OAuth2 credentials file
token_path: Path to store/load access token
"""
if not GOOGLE_API_AVAILABLE:
raise ImportError("Google API libraries not installed")
self.credentials_path = credentials_path
self.token_path = token_path
self.creds = None
self.service = None
self.calendar_id = 'primary' # Use primary calendar by default
def authenticate(self) -> bool:
"""
Authenticate with Google Calendar API.
Returns:
True if authentication successful, False otherwise
"""
try:
# Load existing token if available
if os.path.exists(self.token_path):
self.creds = Credentials.from_authorized_user_file(self.token_path, SCOPES)
# If no valid credentials, get new ones
if not self.creds or not self.creds.valid:
if self.creds and self.creds.expired and self.creds.refresh_token:
self.creds.refresh(Request())
else:
if not os.path.exists(self.credentials_path):
print(f"❌ Credentials file not found: {self.credentials_path}")
print("Please download OAuth2 credentials from Google Cloud Console")
return False
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_path, SCOPES)
self.creds = flow.run_local_server(port=0)
# Save credentials for next run
with open(self.token_path, 'w') as token:
token.write(self.creds.to_json())
# Build service
self.service = build('calendar', 'v3', credentials=self.creds)
print("✅ Google Calendar authenticated")
return True
except Exception as e:
print(f"❌ Google Calendar authentication failed: {str(e)}")
return False
def create_event(self,
title: str,
description: str,
start_time: datetime,
end_time: Optional[datetime] = None,
location: Optional[str] = None,
reminders: Optional[List[int]] = None) -> Dict[str, Any]:
"""
Create a calendar event.
Args:
title: Event title
description: Event description
start_time: Event start time
end_time: Event end time (defaults to start_time + 1 hour)
location: Event location
reminders: List of reminder times in minutes before event
Returns:
Dictionary with event information
"""
if not self.service:
if not self.authenticate():
return {"error": "Failed to authenticate with Google Calendar"}
try:
# Default end time if not provided
if end_time is None:
end_time = start_time + timedelta(hours=1)
# Build event
event = {
'summary': title,
'description': description,
'start': {
'dateTime': start_time.isoformat(),
'timeZone': 'America/Toronto',
},
'end': {
'dateTime': end_time.isoformat(),
'timeZone': 'America/Toronto',
},
}
# Add location if provided
if location:
event['location'] = location
# Add reminders
if reminders:
event['reminders'] = {
'useDefault': False,
'overrides': [
{'method': 'popup', 'minutes': minutes}
for minutes in reminders
],
}
else:
event['reminders'] = {'useDefault': True}
# Create event
created_event = self.service.events().insert(
calendarId=self.calendar_id,
body=event
).execute()
return {
"success": True,
"event_id": created_event['id'],
"event_link": created_event.get('htmlLink'),
"title": title,
"start_time": start_time.isoformat()
}
except HttpError as e:
return {
"error": f"Google Calendar API error: {str(e)}"
}
except Exception as e:
return {
"error": f"Failed to create event: {str(e)}"
}
def add_deadline(self,
course_code: str,
assignment_name: str,
due_date: datetime,
description: str = "",
reminders: Optional[List[int]] = None) -> Dict[str, Any]:
"""
Add a course deadline to Google Calendar.
Args:
course_code: Course code (e.g., "CSC148H1")
assignment_name: Assignment name
due_date: Due date and time
description: Additional description
reminders: List of reminder times in minutes (default: [1440, 60] = 1 day and 1 hour)
Returns:
Dictionary with event information
"""
# Default reminders: 1 day before and 1 hour before
if reminders is None:
reminders = [1440, 60] # 24 hours and 1 hour
# Build title
title = f"[{course_code}] {assignment_name} DUE"
# Build description
full_description = f"Course: {course_code}\nAssignment: {assignment_name}\n"
if description:
full_description += f"\nDetails:\n{description}"
# Create event (deadline is an all-day event or specific time)
return self.create_event(
title=title,
description=full_description,
start_time=due_date,
end_time=due_date,
reminders=reminders
)
def batch_add_deadlines(self, deadlines: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Add multiple deadlines to Google Calendar.
Args:
deadlines: List of deadline dictionaries, each containing:
- course_code: str
- assignment_name: str
- due_date: datetime or ISO string
- description: str (optional)
Returns:
Dictionary with results for each deadline
"""
results = {
"total": len(deadlines),
"successful": 0,
"failed": 0,
"events": []
}
for deadline in deadlines:
# Parse due_date if it's a string
due_date = deadline['due_date']
if isinstance(due_date, str):
try:
due_date = datetime.fromisoformat(due_date.replace('Z', '+00:00'))
except:
results["failed"] += 1
results["events"].append({
"course_code": deadline['course_code'],
"assignment_name": deadline['assignment_name'],
"error": "Invalid date format"
})
continue
# Add deadline
result = self.add_deadline(
course_code=deadline['course_code'],
assignment_name=deadline['assignment_name'],
due_date=due_date,
description=deadline.get('description', '')
)
if "error" in result:
results["failed"] += 1
else:
results["successful"] += 1
results["events"].append(result)
return results
def list_upcoming_events(self, max_results: int = 10) -> List[Dict[str, Any]]:
"""
List upcoming events from Google Calendar.
Args:
max_results: Maximum number of events to return
Returns:
List of event dictionaries
"""
if not self.service:
if not self.authenticate():
return []
try:
now = datetime.utcnow().isoformat() + 'Z'
events_result = self.service.events().list(
calendarId=self.calendar_id,
timeMin=now,
maxResults=max_results,
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
return [
{
"title": event['summary'],
"start": event['start'].get('dateTime', event['start'].get('date')),
"end": event['end'].get('dateTime', event['end'].get('date')),
"description": event.get('description', ''),
"link": event.get('htmlLink')
}
for event in events
]
except HttpError as e:
print(f"❌ Failed to list events: {str(e)}")
return []