PythonとGoogle APIを使ってCSVの予定でGoogleカレンダーを更新してみた

目標：Googleカレンダー⇔CSVファイル⇔別アプリのようにカレンダーと他アプリを連携すること。
以下の続編。
>Pythonを使ってCSVの予定をGoogleカレンダーに載っけてみた
>https://qiita.com/Octpascal/items/07e53bd89dfbca93bf3e

CSVのファイルに記述した予定でGoogleカレンダーを更新するプログラムを作りました。
**現状、CSV⇒Googleカレンダーの一方向です。**

CSVファイルは前回と変わらず、以下の記述

```csv
id,cid,summary,location,description,start,end
```
- id : CSV内で識別用ID(str)
- cid : カレンダーに登録したときにGoogleカレンダー上で決定されるID(str)
- summary : イベント名(str)
- location : 開催場所(str)
- description : 詳細(str)
- start : 開始時間(str 例：2020/08/22 09:00 もしくは 2020/08/22)
- end : 終了時間(str 開始時刻と同じ記述。ただし、揃えなければならない)

# Googleカレンダー操作用コード
```Python
from __future__ import print_function
import datetime
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar']
CID = 'Calendar ID'

def check_token():
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    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)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)

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

def insert(event):
    service = check_token()
    # Call the Calendar API
    create_event = service.events().insert(calendarId=CID, body=event).execute()
    return create_event['id']

def get_lists():
    service = check_token()

    page_token = None
    events = []
    while True:
        events_list = service.events().list(calendarId=CID, pageToken=page_token).execute()
        page_token = events_list.get('nextPageToken')
        events.extend(events_list['items'])
        if not page_token:
            break
    return events
    
def update(eid, event):
    service = check_token()
    updated_event = service.events().update(calendarId=CID, eventId=eid, body=event).execute()
    return (updated_event['updated'])

def delete(eid):
    service = check_token()
    service.events().delete(calendarId=CID, eventId=eid).execute()
```
CIDには自身のカレンダーIDを入れてください。

### check_token
GoogleカレンダーAPIのサンプルコードから。APIの認証用。

### insert
イベント登録。辞書型を受け取り投げる。

### get_list
既に登録されているイベントの取得。一度に取得できるリスト数に制限があるが、nextPageTokenを送ることで続きのリストを取得できる。

### update, delete
そのままの用途

# CSVをAPI用に変換するプログラム
```Python
import pandas as pd
import datetime
import gg_calendar
import sys
TIMEZONE = 'Japan'

def TransTime(t):
    if len(t) > 11:
        strtime = datetime.datetime.strptime(t, '%Y/%m/%d %H:%M').isoformat()
        dictime = {
            'dateTime': strtime,
            'timeZone': TIMEZONE
        }
    else :
        strtime = datetime.datetime.strptime(t, '%Y/%m/%d').isoformat()
        dictime = {
            'date': strtime[0:10],
            'timeZone': TIMEZONE
        }
    return dictime

def get_list():
    lists = gg_calendar.get_lists()
    new_lists = []
    for event in lists :
        new_lists.append(
            {
                'id': event['id'],
                'summary': event['summary'],
                'location': event['location'],
                'description': event['description'],
                'start': event['start'],
                'end': event['end']
            }
        )
    return new_lists

def search_id(dics, skey, svalue):
    df = pd.DataFrame(dics)
    return (df[skey] == svalue).any()

csv = pd.read_csv('csv.csv')
csv_d = csv.to_dict(orient='index')

ex_lists = get_list()

# delete
for ex_list in ex_lists:
    if not search_id(csv, 'cid', ex_list['id']):
        print("delete from calendar id={}".format(ex_list['id']))
        gg_calendar.delete(ex_list['id'])

# update, insert
for h, num in zip(csv_d.values(), csv_d.keys()):
    h['start'] = TransTime(h['start'])
    h['end']   = TransTime(h['end'])

    if (search_id(ex_lists, 'id', h['cid'])):
        del h['id']
        cid = h['cid']
        del h['cid']
        print('updata')
        gg_calendar.update(cid, h)
    else:
        del h['id']
        del h['cid']
        try:
            event_id = gg_calendar.insert(h)
        except:
            print("Error: Can't put id={} on your calendar".format(csv.loc[num, 'id']))
        else:
            print("Put id={} on your calendar as calendar_id={}".format(csv.loc[num, 'id'], event_id))
            csv.loc[num, 'cid'] = event_id

csv.to_csv('csv.csv',index=False)
```

### TransTime
上記CSVの書式をGoogleカレンダーAPIの書式へ変換。詳細は[前回記事](https://qiita.com/Octpascal/items/07e53bd89dfbca93bf3e)参照。

### get_lsit
リストを取得し必要な情報だけの辞書型にする。

### search_id
**辞書に値があるかどうか判定する**
ある辞書型(dics)の辞書キー(skey)にある値(svalue)があるかどうか判定する。
辞書型をpandas DataFrameに変換する。
`df[skey] == svalue`でBooleanのpandas Seriesが返ってくる。(例：True, False, False, False)
any()関数でひとつでもTrueがあればTrueとなることで判定できる。

最初は辞書型のままfor文を回していたが、多分これが一番簡単に記述できます。

### delete
CSVから消えたものはカレンダーから消します。

### update, insert
既にカレンダーに登録されているものは更新。ないものは登録し、返ってきたイベントIDをCSVに追記します。

# 現状バグ
- カレンダーにひとつも登録がないとエラーが発生する。
- カレンダー内の登録がget_list関数で作成する辞書型の十分条件。例えば、カレンダーにlocationのないイベントがあるとエラーが発生する。⇒現状、id以外利用してないので取得しないことにすると解決する。

# 改善すべき点
- 既存の登録されているカレンダーをすべてupdateに投げている。⇒終了時刻が一定以上前のデータは更新しないなど対策する。
- カレンダーの既存イベントを全取得している、⇒ こちらもある程度期限を決めて取得することにする。

# さいごに
次回は上記点を改善しアップデートしていきたい。