|
| 1 | +import os |
| 2 | +from datetime import datetime |
| 3 | +from google.auth.transport.requests import Request |
| 4 | +from google.oauth2.credentials import Credentials |
| 5 | +from google_auth_oauthlib.flow import InstalledAppFlow |
| 6 | +from googleapiclient.discovery import build |
| 7 | + |
| 8 | +SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] |
| 9 | + |
| 10 | + |
| 11 | +def _get_calendar_service(): |
| 12 | + """Internal — authenticates and returns Google Calendar service.""" |
| 13 | + creds = None |
| 14 | + |
| 15 | + if os.path.exists("token.json"): |
| 16 | + creds = Credentials.from_authorized_user_file("token.json", SCOPES) |
| 17 | + |
| 18 | + if not creds or not creds.valid: |
| 19 | + if creds and creds.expired and creds.refresh_token: |
| 20 | + creds.refresh(Request()) |
| 21 | + else: |
| 22 | + flow = InstalledAppFlow.from_client_secrets_file( |
| 23 | + "credentials.json", SCOPES |
| 24 | + ) |
| 25 | + creds = flow.run_local_server(port=0) |
| 26 | + |
| 27 | + with open("token.json", "w") as token: |
| 28 | + token.write(creds.to_json()) |
| 29 | + |
| 30 | + return build("calendar", "v3", credentials=creds) |
| 31 | + |
| 32 | +def import_commitments_from_google_calendar(start_date, end_date): |
| 33 | + """ |
| 34 | + Imports calendar events as commitments from Google Calendar. |
| 35 | +
|
| 36 | + Fetches all events between start_date and end_date and converts |
| 37 | + them into a commitments dictionary mapping dates to blocked hours. |
| 38 | +
|
| 39 | + Note: Requires a live Google Calendar connection and valid credentials. |
| 40 | + Authentication is handled via OAuth 2.0. On first run, a browser window |
| 41 | + will open asking the user to grant calendar access. |
| 42 | +
|
| 43 | + Args: |
| 44 | + start_date (date): First day to fetch events from. |
| 45 | + end_date (date): Last day to fetch events until. |
| 46 | +
|
| 47 | + Returns: |
| 48 | + tuple: A tuple containing two dictionaries: |
| 49 | + - commitments (dict): Mapping dates to total blocked hours that day. |
| 50 | + - event_name (dict): Mapping dates to lists of event titles. |
| 51 | +
|
| 52 | + Example: |
| 53 | + >>> from datetime import date, datetime |
| 54 | + >>> commitments, event_name = import_commitments_from_google_calendar( |
| 55 | + ... start_date=date(2026, 5, 13), |
| 56 | + ... end_date=date(2026, 6, 5) |
| 57 | + ... ) |
| 58 | + """ |
| 59 | + service = _get_calendar_service() |
| 60 | + |
| 61 | + start = datetime.combine(start_date, datetime.min.time()).isoformat() + "Z" |
| 62 | + end = datetime.combine(end_date, datetime.min.time()).isoformat() + "Z" |
| 63 | + |
| 64 | + events_result = service.events().list( |
| 65 | + calendarId="primary", |
| 66 | + timeMin=start, |
| 67 | + timeMax=end, |
| 68 | + singleEvents=True, |
| 69 | + orderBy="startTime" |
| 70 | + ).execute() |
| 71 | + |
| 72 | + events = events_result.get("items", []) |
| 73 | + |
| 74 | + commitments = {} |
| 75 | + event_name = {} |
| 76 | + for event in events: |
| 77 | + if "dateTime" not in event["start"]: |
| 78 | + continue |
| 79 | + |
| 80 | + start_time = datetime.fromisoformat( |
| 81 | + event["start"]["dateTime"].replace("Z", "+00:00") |
| 82 | + ) |
| 83 | + end_time = datetime.fromisoformat( |
| 84 | + event["end"]["dateTime"].replace("Z", "+00:00") |
| 85 | + ) |
| 86 | + duration_hours = (end_time - start_time).seconds / 3600 |
| 87 | + event_date = start_time.date() |
| 88 | + event_title = event.get("summary", "Unnamed event") |
| 89 | + |
| 90 | + #to prevent overwriting existing commitments, sum durations for events on the same day |
| 91 | + if event_date in commitments: |
| 92 | + commitments[event_date] += duration_hours |
| 93 | + event_name[event_date].append(event_title) |
| 94 | + else: |
| 95 | + commitments[event_date] = duration_hours |
| 96 | + event_name[event_date] = [event_title] |
| 97 | + |
| 98 | + return (commitments, event_name) |
| 99 | + |
| 100 | + |
0 commit comments