Google Calendar APIで複数カレンダーを統合管理する自動化システム

Google Calendar APIで複数カレンダーを統合管理する自動化システム

仕事用カレンダー、個人カレンダー、副業用カレンダーと複数のGoogleカレンダーを使っていると、「今週の空き時間はどこか」を確認するのが面倒になった。PythonとGoogle Calendar APIを使って、複数カレンダーを一元管理するシステムを作った。

Google Calendar APIの準備

Google Cloud Consoleでの設定

1. Google Cloud Console でプロジェクトを作成

2. 「APIとサービス」→「ライブラリ」でGoogle Calendar APIを有効化

3. 「認証情報」→「OAuth 2.0クライアントID」を作成(デスクトップアプリ)

4. credentials.jsonをダウンロード

必要なライブラリのインストール

pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client

認証フローの実装

import os
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

def get_calendar_service():
    creds = None

    # トークンが存在する場合は読み込む
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)

    # トークンがないか期限切れの場合は再認証
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES
            )
            creds = flow.run_local_server(port=0)

        # トークンを保存
        with open('token.json', 'w') as token:
            token.write(creds.to_json())

    return build('calendar', 'v3', credentials=creds)

初回実行時にブラウザが開いてGoogleアカウントへの認可を求められる。2回目以降はtoken.jsonを使って自動認証される。

複数カレンダーの予定を一括取得

from datetime import datetime, timedelta
import pytz

def get_all_events(service, calendar_ids: list, days_ahead: int = 7):
    """
    複数カレンダーから指定日数分の予定を取得する

    Args:
        service: Google Calendar APIのサービスオブジェクト
        calendar_ids: カレンダーIDのリスト
        days_ahead: 何日先まで取得するか

    Returns:
        日時でソートされた予定のリスト
    """
    jst = pytz.timezone('Asia/Tokyo')
    now = datetime.now(jst)
    time_min = now.isoformat()
    time_max = (now + timedelta(days=days_ahead)).isoformat()

    all_events = []

    for calendar_id in calendar_ids:
        try:
            events_result = service.events().list(
                calendarId=calendar_id,
                timeMin=time_min,
                timeMax=time_max,
                maxResults=100,
                singleEvents=True,
                orderBy='startTime'
            ).execute()

            events = events_result.get('items', [])

            for event in events:
                # カレンダーIDを付加して区別できるようにする
                event['_calendar_id'] = calendar_id
                all_events.append(event)

        except Exception as e:
            print(f"カレンダー {calendar_id} の取得に失敗: {e}")

    # 開始時刻でソート
    all_events.sort(key=lambda e: e.get('start', {}).get('dateTime',
                                       e.get('start', {}).get('date', '')))

    return all_events

カレンダーIDの取得方法

def list_calendars(service):
    """ユーザーが持つカレンダーの一覧を表示する"""
    calendars = service.calendarList().list().execute()

    for calendar in calendars.get('items', []):
        print(f"名前: {calendar['summary']}")
        print(f"ID: {calendar['id']}")
        print("---")

カレンダーIDはprimary(メインカレンダー)かxxxx@group.calendar.google.comの形式。

空き時間を計算する

複数カレンダーの予定を統合した上で、空き時間を計算する:

def find_free_slots(events: list, work_start_hour: int = 9,
                    work_end_hour: int = 18, min_duration_minutes: int = 60):
    """
    予定の隙間にある空き時間を検出する

    Args:
        events: get_all_eventsで取得した予定リスト
        work_start_hour: 作業開始時刻(時)
        work_end_hour: 作業終了時刻(時)
        min_duration_minutes: 空き時間の最小時間(分)

    Returns:
        空き時間のリスト [(開始datetime, 終了datetime), ...]
    """
    jst = pytz.timezone('Asia/Tokyo')
    today = datetime.now(jst).date()
    free_slots = []

    # 日付ごとに予定をグループ化
    from collections import defaultdict
    events_by_date = defaultdict(list)

    for event in events:
        start = event.get('start', {})
        if 'dateTime' in start:
            start_dt = datetime.fromisoformat(start['dateTime'])
            events_by_date[start_dt.date()].append(event)

    # 各日の空き時間を計算
    for day_offset in range(7):
        target_date = today + timedelta(days=day_offset)
        day_events = events_by_date.get(target_date, [])

        # 作業時間帯を設定
        work_start = jst.localize(datetime.combine(target_date,
                                                    datetime.min.time().replace(hour=work_start_hour)))
        work_end = jst.localize(datetime.combine(target_date,
                                                  datetime.min.time().replace(hour=work_end_hour)))

        # 予定の開始・終了時刻を収集
        busy_times = []
        for event in day_events:
            start_str = event.get('start', {}).get('dateTime')
            end_str = event.get('end', {}).get('dateTime')
            if start_str and end_str:
                busy_times.append((
                    datetime.fromisoformat(start_str),
                    datetime.fromisoformat(end_str)
                ))

        busy_times.sort()

        # 隙間を計算
        current = work_start
        for busy_start, busy_end in busy_times:
            if busy_start > current:
                duration = (busy_start - current).total_seconds() / 60
                if duration >= min_duration_minutes:
                    free_slots.append((current, busy_start))
            current = max(current, busy_end)

        # 最後の予定から作業終了まで
        if current < work_end:
            duration = (work_end - current).total_seconds() / 60
            if duration >= min_duration_minutes:
                free_slots.append((current, work_end))

    return free_slots

使い方

def main():
    service = get_calendar_service()

    # カレンダーIDを設定(list_calendarsで確認したIDを使う)
    CALENDAR_IDS = [
        'primary',                              # メインカレンダー
        'work@group.calendar.google.com',       # 仕事用
        'personal@group.calendar.google.com',   # 個人用
    ]

    # 1週間分の予定を取得
    print("予定を取得中...")
    events = get_all_events(service, CALENDAR_IDS, days_ahead=7)

    print(f"\n今後7日間の予定: {len(events)}件")
    for event in events:
        start = event['start'].get('dateTime', event['start'].get('date'))
        print(f"  - {start}: {event.get('summary', '(タイトルなし)')}")

    # 空き時間を計算
    free_slots = find_free_slots(events, min_duration_minutes=60)

    print(f"\n1時間以上の空き時間: {len(free_slots)}件")
    for start, end in free_slots:
        duration = int((end - start).total_seconds() / 60)
        print(f"  - {start.strftime('%m/%d %H:%M')}〜{end.strftime('%H:%M')} ({duration}分)")

if __name__ == "__main__":
    main()

まとめ

Google Calendar APIで複数カレンダーを統合したことで:

  • 「今週どこが空いているか」を1コマンドで確認できる
  • 複数のカレンダーをまたいだ空き時間の計算ができる
  • 日程調整の返信が速くなった
  • 応用として、空き時間をSlackに通知したり、Googleスプレッドシートに書き出して共有したりもできる。OAuth認証さえ乗り越えれば、あとのAPIは直感的に使いやすい。