Google Calendar APIを使ったToDoリストアプリの作成

この記事は kstm Advent Calendar 2023 の 20 日目の記事です．

https://qiita.com/advent-calendar/2023/kstm

# はじめに
あなたは普段どのようにタスクを管理していますか？
私は今まで windows に付属している「[付箋（Sticky Notes）](https://apps.microsoft.com/detail/9NBLGGH4QGHW?activetab=pivot%3Aoverviewtab&hl=ja-jp&gl=JP)」を使っていました．![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627615/b0e81329-9e61-504d-39bb-2ebbcf0e65d0.png)

このように日付と予定を書いたメモを画面の端っこに置いていましたが，流石にこの程度の機能ならもっと使いやすいアプリを自分で作れるのでは？と思い今回のアプリを作成しました．

**※注意　プログラミング初心者によるアプリ開発となります．**

# アプリの概要
作成したアプリの一通りの機能です．
![アプリ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627615/e52a295e-c58e-85cf-96cc-ba3c5ebcc4e7.gif)
- タスクの読み込み
- タスクの追加
- タスクの詳細表示
- タスクの完了，未完了切り替え
- タスクの内容変更
- タスクの削除

ToDoリストアプリとしては最低限の機能を持っていると思います．

タスクの追加では，タスクの内容を入力すると多少曖昧でも ChatGPT がいい感じに整えてくれます．

また，このタスクの情報はgoogleカレンダー上にイベントとして記録されているため，好きなデバイスからそのカレンダーを確認することができます．

# 使用技術
- Python 3.10.5
- [Google Calendar API](https://developers.google.com/calendar/api/guides/overview)
- [ChatGPT API](https://platform.openai.com/docs/introduction)
- [Flet](https://flet.dev/)

Google Calendar API は google カレンダー上に，タスクをイベントとして管理するために使用します．API を使用するためには[Google Cloud Platform](https://console.cloud.google.com)で手続きを行う必要があります．

こちらのサイトを参考にしました．**サービスアカウントを使用するため，「[補足：サービスアカウントを使ったやり方](https://www.coppla-note.net/posts/tutorial/google-calendar-api/#%E8%A3%9C%E8%B6%B3%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%82%84%E3%82%8A%E6%96%B9)」の部分も参考にしてください．**

https://www.coppla-note.net/posts/tutorial/google-calendar-api

ChatGPT API はタスクの新規作成を行う際に，多少適当な内容の入力でもイベントの作成に適したデータに整えるために使用します．

Flet は python のフレームワークで，素早くクロスプラットフォームなアプリを作成できることが売りらしいです．

こちらが Flet について参考にした記事です．

https://qiita.com/NasuPanda/items/48849d7f925784d6b6a0


# コードの説明
ソースコードは記事に載せるにはちょっと長いので github に公開しておきます．

https://github.com/ogaog/MyTaskApp

### ファイル構成
```
MyTaskApp/
├── .env
├── credentials_service.json
├── serviceApi.py
└── window.py
```
- **.env**
タスクを管理する google カレンダーのIDと，ChatGPT の API を保存しておくファイル．serviceApi.py で呼び出される．
- **credentials_service.json**
Google Cloud Platform で OAuth Client を作成した時にダウンロードした Json ファイル．serviceApi.py で呼び出される．
- **serviceApi.py**
API を扱う関数をまとめたファイル．window.py でインポートされる．
- **window.py**
アプリを動かすメインのファイルで，このファイルを実行する．Flet を用いた GUI の記述がほとんど．

### よく出てくるデータ
[google calendar api で扱う **event** ](https://developers.google.com/calendar/api/v3/reference/events)は次のような dict 型で，一つのイベント情報を記録しています．serviceApi.py , window.py に出てくる ``event`` , ``*_event`` という変数にはこのデータが入るようにしたはずです．
```python
{
  "id": string,
  "summary": string,
  "description": string,
  "start": {
    "dateTime": datetime
  },
  "end": {
    "dateTime": datetime
  },
  その他様々なデータ...
}
```

## service.py
API を扱う関数をまとめたファイルで，window.py でインポートされて使われます．

ここでは主要な関数の説明をします．

### get_events
```python
def get_events():
    """現在時刻以降に始まるイベント情報を取得する.

    Returns:
        None: イベントが存在しなかった場合Noneが返される.
        dict: イベントが存在する場合,日付(yyyy-mm-dd)がkey,その日付に始まるイベントが
        格納されたリストがvalueとなる辞書events_dictが返される.
    """    
    now = datetime.datetime.utcnow().isoformat() + 'Z'

    # カレンダー上のイベント情報を取得
    events_result = service.events().list(
        calendarId=MY_CALENDAR_ID, 
        timeMin=now, 
        maxResults=70, 
        singleEvents=True,
        orderBy='startTime'
        ).execute()  
    events = events_result.get('items', []) # eventsは取得したイベント情報(dict型)が格納されたリストである.

    if not events:
        return None
    else:
        events_dict = {}
        for event in events:
            # イベントの開始日付(yyyy-mm-dd)を取得し,同じ日付のところのリストにイベントを追加していく.
            date = event['start'].get('dateTime', event['start'].get('date'))[:10]
            if date in events_dict:
                events_dict[date] += [event]
            else:
                events_dict[date] = [event]
        return events_dict
```
google カレンダー上のイベントを``service.events().list().execute()``で取得して，辞書に日付ごとにeventをまとめて返します．
```python:events_dict
{
  '2023-12-20': [{イベント情報},{イベント情報}],
  '2023-12-22': [{イベント情報}],
  '2023-12-29': [{イベント情報},{イベント情報},{イベント情報}]
}
```

### switch_done
```python
def switch_done(event):
    """イベント(タスク)の完了,未完了状態を切り替える.
    タスクが終わっているかどうかは,イベント情報の'transparency'の値で記録する.
    イベントの'transparency'の値が'transparent'(完了状態)か'opaque'(未完了状態)の
    どちらなのかを調べ,反転させる.

    Args:
        event (dict): 一つのイベント情報が記録されている辞書.

    Returns:
        dict: カレンダー上のイベントの'transparency'の値を変更し,
        アップデートされたイベント情報updated_eventが返される.
    """    
    event_id = event['id']

    # デフォルトのイベント情報では'transparency'というkeyが存在しないため'opaque'を取得することになる.
    current_transparency = event.get('transparency', 'opaque')

    new_transparency = 'transparent' if current_transparency == 'opaque' else 'opaque' # 状態を反転.
    event['transparency'] = new_transparency

    # カレンダー上のイベント情報を変更する
    updated_event = service.events().update(
        calendarId=MY_CALENDAR_ID, 
        eventId=event_id, 
        body=event
        ).execute()
    return updated_event
```
タスクが完了しているかどうかは event の 'transparency' の値で記録しています．それを切り替える関数です．``service.events().update().execute()`` でイベントをアップデートします．

### delete_event
```python
def delete_event(event):
    """イベントを削除する.

    Args:
        event (dict): 削除するイベントの情報.イベント情報からidを取得し,カレンダー上のイベントを削除する.

    Returns:
        str: 完了メッセージが返される.
    """    
    # カレンダー上のイベントを削除
    service.events().delete(calendarId=MY_CALENDAR_ID, eventId=event['id']).execute()
    return f"Event with ID {event['id']} has been deleted."
```
``service.events().delete().execute()`` でイベントを削除します．

### add_event
```python
def add_event(event):
    """イベントを追加する.

    Args:
        event (dict): 追加するイベントの情報.

    Returns:
        dict: 追加したイベントの情報added_eventが返される.
    """    
    # イベントを追加
    added_event = service.events().insert(calendarId=MY_CALENDAR_ID, body=event).execute()
    return added_event
```
``service.events().insert().execute()``でイベントを追加します．

### arrange_input
```python
def arrange_input(prompt):
    """タスクの内容をChatGPTに渡し,googleカレンダーのイベントとして追加できるように情報を整えてもらう関数.
    例として,"大掃除 12月31日15時 ぞうきん,スプレー買う"というpromptで,
    summary(イベント名):大掃除
    start(開始日時):12-31 12:00
     end (終了日時):12-31 15:00
    description(メモ):ぞうきん,スプレー買う
    というイベント情報(dict)が生成され,返される.
    締め切り日時はendとして,締切日時の3時間前をstartとして設定する.

    Args:
        prompt (str): ChatGPTに渡す入力データ. タスクの内容を「<name><date/time information><memo>」
        という順番の形式で受け取り,<name>はタスクの名前,<date/time information>はタスクの締切日時,
        <memo>はタスクのメモを表す.

    Returns:
        dist: ChatGPTが整えたデータを元に作成したイベント情報が返される.
    """    

    # ChatGPTにpromptを渡し,イベント情報を整えてもらう.
    response = openai.chat.completions.create(
        model = "gpt-3.5-turbo",
        messages = [
            {"role": "system", "content": (
                "You are a program that outputs received data in the appropriate format. "
                "The received data consists of [<name><date/time information><memo>]."
                " You output this data in the format {'summary':'<name>','date':'yyyyy-mm-dd','time':'hh:mm','description':'<memo>','pre3h_date':'yyyyy-mm-dd','pre3h_time':'hh:mm'}."
                f"Note that the date and time to be output are inferred from the <date/time information> and the current time ({datetime.datetime.now()})."
                "The date and time three hours before ('date':'yyyyyy-mm-dd','time':'hh:mm') shall be ('pre3h_date':'yyyyyy-mm-dd','pre3h_time':'hh:mm')."
                "If no time is specified, 23:59 is assumed. If no date is specified, the current date is assumed."
                "If <memo> is not present, 'description':'None' is assumed."
            )},
            {"role": "user", "content": prompt}
        ],
        temperature=0
    )

    # ChatGPTからのメッセージを取得.
    data_msg = response.choices[0].message.content
    format_data = ast.literal_eval(data_msg) # dict型に変換

    # イベント情報としてdraft_eventを作成する.
    draft_event = {
        'summary': f'{format_data["summary"]}',
        'description': f'{format_data["description"]}',
        'start': {
            'dateTime': f'{format_data["pre3h_date"]}T{format_data["pre3h_time"]}:00+09:00'
        },
        'end': {
            'dateTime': f'{format_data["date"]}T{format_data["time"]}:00+09:00'
        }
    }
    return draft_event
```
``prompt``に，タスクの内容が (name)(date/time information)(memo) という順番で入力されるのを前提にイベント情報の辞書を作成する．

## window.py
このファイルを実行することでアプリが動きます．Flet を使って GUI がつらつら書かれています．解説は[コード](https://github.com/ogaog/MyTaskApp/blob/master/window.py)中のコメントと関数のdocstringを参照してください．リファレンスも置いておきます．

https://flet.dev/docs/

これを[exeファイル化](https://flet.dev/docs/guides/python/packaging-desktop-app)して，同ディレクトリ下に``.env``，``credentials_service.json``を置いても動かすことができます．([web アプリとしてデプロイする](https://flet.dev/docs/guides/python/deploying-web-app)ことも可能らしい？）


# 備考　2023-12-20
対応していないエラー処理があります．気が向いたら修正します．
- [google calendar api のエラー](https://developers.google.com/calendar/api/guides/errors)
- ChatGPT の出力エラー

# おわりに
ここまで読んで頂きありがとうございました．初めての記事となるので拙い部分もあったかと思いますが，何かの参考になれば幸いです．