昔作ったGoogle Calendar Event登録ScriptをPythonで全面的に書き換えるべく作業を始めようと思ったはいいがもう何年も前なので完全にやり方を忘れた。またどうせ忘れるのでここにメモしておこうと思う。
大まかな流れは以下のようになる。
- プロジェクトの作成
- 認証情報を作成
- 認証情報を使ってAPIを叩く
認証情報を作成
まずGoogle Cloud Platformにアクセスして認証情報の項目を開く。ちなみにこのGoogle Cloud Platformという名前も昔はGoogle Developer Consoleとかだったのでまた必要になった頃には変わってる可能性もある。さてプロジェクトの作成は特に問題ないだろう。次に認証情報を作成というボタンを押したら以下の3種類が出てくる。昔はOAuth一択だった気がするが現状では3種類あるようだ。認証の概要に詳細な説明があるが難しい事はさておいて要するにAPIを叩くにあたってユーザー認証が必要になるがそれをどうするかの違い。
- API キー
- 一般公開データにユーザー認証なしでアクセスする。逆に言うとユーザー認証が必要なデータにはアクセス出来ない。
- OAuth2.0クライアント
- 何処のサービスでもよく見る奴。トークンでユーザー認証する。何かするたびにこのトークンが必要。しかしこのOAuth同意画面の通過がいつの間にか非常に面倒臭くなった。個人でちょっと使うようなレベルの面倒さじゃない。どうも個人データを扱うAPIを有効にしてるとGoogle側の審査を求められるようになったらしい。公開ステータスをテストのままにしとけば一応使えはするが、ユーザーの種類が外部でかつ公開ステータスをテストのままにしておくとReflesh Tokenの有効期限が7日となる。ちょっと放っておくとすぐ期限切れになって面倒臭い。ユーザーの種類を内部に変えればいいんだろうがGoogle Workspaceユーザーしか出来ない。じゃあ対処法はと言うと一旦token.jsonを削除して取得し直ししかないようだ。
- サービスアカウント
- API専用のGoogleアカウントを作ってアクセスする。こいつはパスワードがないので通常のアカウントとしては使えない。アクセス先に認証ユーザー登録してもらう事が必要。今回の場合カレンダーごとに共有アカウントとしてサービスアカウントを予め手動で追加してやらねばならない。
OAuth2.0とサービスアカウントを併用する場合はcalendarのアクセス権限にくれぐれも注意する事。サービスアカウント権限で作成したcalendarはOAuth2権限からは見えないしその逆もある。考えてみれば当然の話。要するにサービスアカウントとはAccess Control Ruleで明示的に許可された事しか出来ない単なる一般ユーザー。Settingsの取得はおろかCalendarListの取得すらできない。
準備としてまず
- 認証情報のKeyをJSONでダウンロードしておく。
- APIとサービス > ライブラリからGoogle Calendar APIを有効化。
- Google関連のPython Packageをインストール。google-api-python-client、google-auth-httplib2、google-auth-oauthlibが必要。
OAuth2.0
使う言語はPythonなのでPython Quickstartを参考にする。サンプル読めば大体分かる。
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())
まあ要するに有効なrefresh_tokenを持っているかどうかで分岐させている。
サービスアカウント
こいつがquickstartに全く載ってないのであちこち探してなにやらこんな感じでいいらしいことを知る。
creds = Credentials.from_service_account_file("credentials.json") creds = creds.with_scopes(["https://www.googleapis.com/auth/calendar"])
シンプルだし楽でいいね。ちなみにこれだとcreds.validがFalseを返す。どうもtokenを持ってないかららしい。OAuth2.0同様
creds.refresh(Request())
とやってやればいいだけなんだけど別にtokenがなくてもvalidがFalseでも立派に使える。
APIを叩く
サンプルを読むとまずservice とやらを作ってる。
service = build("calendar", "v3", credentials=creds)
これが何をしてるのかまず分からん。仕方ないのでgoogleapiclientのcodeを読んでみると細かい途中経過はさておいてこいつはResourceオブジェクトとやらを返すらしい。
now = datetime.datetime.utcnow().isoformat() + "Z" print("Getting the upcoming 10 events") events_result = service.events().list(calendarId="primary", timeMin=now, maxResults=10, singleEvents=True, orderBy="startTime").execute()
events()ってなんなんと1時間ほど悩んでgoogleapiclientのcodeを検索しまくったが分かってしまえばなんて事ない。Google CalendarのAPIを叩いてるだけだった。という事は上記のResourceオブジェクトとやらはAPIを叩くインターフェイスであって中身を理解する必要は特になさげ。
ちなみにこれはquickstartに載ってる物なのでOAuth2.0の物だがサービスアカウントでやる場合calendarIdはprimaryではなくちゃんと指定する事。
サービスアカウントで出来ない事
- 当然だがaclへのサービスアカウント追加はサービスアカウントでは出来ない。OAuth2が必要。
- calendarListの取得はOAuth2じゃないと出来ないらしい。実行自体は出来るがちゃんとcalendarのaclにサービスアカウントを追加してるのにitemsが空で返ってくる。
結局サービスアカウントというのは要するに単なる一般ユーザーアカウントの域を出ないんだろう。
Google Calendar API
- event作成を一度に大量にやるとたまに404が返ってくる事がある。アクセス拒否という訳ではなくすぐに問題なく実行できる。
- calendar作成を一度に大量にやるとアクセス拒否されるようになる。たぶん丸一日ぐらい?しきい値は25ぐらいというのも見たが未確認。テスト実行中はくれぐれも注意する事。
Acl
Access Control Rule。設定項目でいう所の「特定のユーザーとの共有」の事。
- delete
- Access Control Ruleを削除する。
service.acl().delete(calendarId="primary", ruleId="ruleId").execute()
- get
- 指定したruleIdのAccess Control Ruleを取得する。
service.acl().get(calendarId="primary", ruleId="ruleId").execute()
- insert
- Access Control Ruleを作成する。
rule = { "scope": { "type": "scopeType", "value": "scopeEmail", }, "role": "role" } service.acl().insert(calendarId="primary", body=rule).execute()
- list
- Access Control RuleのListを取得する。
service.acl().list(calendarId="primary").execute()
- patch
- 指定したruleIdのAccess Control RuleをPATCH Methodで更新する。
rule = service.acl().get(calendarId="primary", ruleId="ruleId").execute() service.acl().update(calendarId="primary", ruleId=rule["id"], body={"role":"newRole"}).execute()
- update
- 指定したruleIdのAccess Control RuleをPUTMethodで更新する。
rule = service.acl().get(calendarId="primary", ruleId="ruleId").execute() rule["role"] = "newRole" service.acl().update(calendarId="primary", ruleId=rule["id"], body=rule).execute()
- watch
- Access Control Ruleの変更を監視する。
calendarList
- delete
- calendarListからcalendarを削除する。あくまでListから消すのであってcalendar自体を消すのではない。
service.calendarList().delete(calendarId="hoge").execute()
- get
- calendarのListとそのmeta dataを取得する。
service.calendarList().get(calendarId="primary").execute()
- insert
- calendarListに既存のcalendarを追加する。新しく作成するのはcalendarsのinsert method。
body = { "id": "hoge" } service.calendarList().insert(body=body).execute()
- list
- calendarListをmeta dataと共に取得する。引数のpageTokenはいわゆる次のページという奴。calendarListの数が大きい時に使うはずだがそんな大きな数には普通はならない。使うとすればeventsのlistだろう。
page_token = None while True: calendar_list = service.calendarList().list(pageToken=page_token).execute() for calendar_list_entry in calendar_list["items"]: print(calendar_list_entry["summary"]) page_token = calendar_list.get("nextPageToken") if not page_token: break
- patch
- calendarList上のcalendarをPATCH Methodで更新する。
calendar_list_entry = service.calendarList().get(calendarId="calendarId").execute() service.calendarList().patch(calendarId=calendar_list_entry["id"], body={"colorId":17}).execute()
- update
- calendarList上のcalendarをPUT Methodで更新する。patchもupdateも挙動は似てるがpatchはPATCH MethodでupdateはPUT Methodを使用している。従ってupdateは全ての要素を指定しないと指定したもの以外はクリアされてdefaultに戻る。patchとupdate共にcalendarIdがprimaryでは通らない。primaryでもきちんと指定する必要がある。
calendar_list_entry = service.calendarList().get(calendarId="calendarId").execute() calendar_list_entry["colorId"] = "newColorId" service.calendarList().update(calendarId=calendar_list_entry["id"], body=calendar_list_entry).execute()
- watch
- CalendarListの変更を監視する。
calendars
- clear
- primary calendar(アカウント紐付けcalendar)内のeventを全消去する。primary専用のMethodであってsecondary calendar(ユーザー作成calendar)には使えない。primary専用にも関わらずcalendarIdを指定しなければならない。
service.calendars().clear(calendarId="primary").execute()
- delete
- secondary calendar自体を削除する。当然だがsecondary calendar専用のMethod。
service.calendars().delete(calendarId="hoge@group.calendar.google.com").execute()
- get
- calendarのmeta dataを取得する。
service.calendars().get(calendarId="primary").execute()
- insert
- calendarを作成する。
body= { "summary": "hoge", "timeZone": "Asia/Tokyo" } service.calendars().insert(body=body).execute()
- patch
- calendarをPATCH Methodで更新する。
calendar = service.calendars().get(calendarId="primary").execute() service.calendars().patch(calendarId=calendar["id"], body={"summary":"hoge"}).execute()
- update
- calendarをPUT Methodで更新する。
calendar = service.calendars().get(calendarId="primary").execute() calendar["summary"] = "hoge" service.calendars().update(calendarId=calendar["id"], body=calendar).execute()
channels
- stop
- watchで設定した監視Channelを停止する。
colors
- get
- calendarとeventのcolor設定を取得する。
service.colors().get().execute()
events
eventのidはbase32hexつまり0-9およびa-vで作成できる5〜1024文字の文字列。これを指定しないと実行するごとに重複eventを作成してしまう。似たような物にiCalUIDがあるがidはcalendar内で一意であればよいが、iCalUIDはcalendar system全体で一意でなければならない。また繰り返しeventは同一のiCalUIDとそれぞれ違うidを持つ。event作成にはどちらか一方があればよいがimportにはiCalUIDが必要。
- delete
- eventを削除するというかeventのstatusをcancelledにする。これを実行してもeventが完全に削除されたわけではない。これを理解しておかないとid指定でevent作成をする時idが重複してエラーとなる。
service.events().delete(calendarId="primary", eventId="hoge").execute()
- get
- eventのmeta dataを取得する。
service.events().get(calendarId="primary", eventId="hoge").execute()
- import
- eventをimportする。
- insert
- eventを作成する。
body = { "summary": "hoge", "start": { "dateTime": "2022-06-01T09:00:00+09:00", "timeZone": "Asia/Tokyo", }, "end": { "dateTime": "2022-06-01T10:00:00+09:00", "timeZone": "Asia/Tokyo", } } service.events().insert(calendarId="primary", body=body).execute()
- instances
- 繰り返しeventの展開した各eventを取得する。
page_token = None while True: events = service.events().instances(calendarId="primary", eventId="eventId", pageToken=page_token).execute() for event in events["items"]: print event["summary"] page_token = events.get("nextPageToken") if not page_token: break
- list
- eventのlistを取得する。取得数はデフォルトだと250。これで足りない場合はmaxResultsで2500まで増やすことが出来るが問題はstartを降順でなどと出来ない事。大体大昔のeventなどどうでもよくて欲しいのは最新のeventなのでその時はtimeMaxもしくはtimeMinで時間指定するしかない。
service.events().list(calendarId="primary").execute()
- move
- 別のcalendarにeventを移動する。
service.events().move(calendarId="primary", eventId="hoge", destination="secondary@group.calendar.google.com").execute()
- patch
- eventをPATCH Methodで更新する。
event = service.events().get(calendarId="primary", eventId="eventId").execute() service.events().update(calendarId="primary", eventId=event["id"], body={"summary":"Appointment at Somewhere"}).execute()
- quickAdd
- 簡単な文字列のみでeventを作成する。文字列をパースしてeventを作成するのだがそんな事わざわざせずともscriptからinsertを叩く分にはそう面倒じゃないので公式の解説もいまいち不親切。
service.events().quickAdd(calendarId="primary", text="Appointment at Somewhere on June 3rd 10am-10:25am").execute()
- update
- eventをPUT Methodで更新する。
event = service.events().get(calendarId="primary", eventId="eventId").execute() event["summary"] = "Appointment at Somewhere" service.events().update(calendarId="primary", eventId=event["id"], body=event).execute()
- watch
- eventの追加またはeventの変更を監視する。
event = { "summary": "Appointment", "location": "Somewhere", "organizer": { "email": "organizerEmail", "displayName": "organizerDisplayName" }, "start": { "dateTime": "2011-06-03T10:00:00.000-07:00" }, "end": { "dateTime": "2011-06-03T10:25:00.000-07:00" }, "attendees": [ { "email": "attendeeEmail", "displayName": "attendeeDisplayName", }, # ... ], "iCalUID": "originalUID" } service.events().import_(calendarId="primary", body=event).execute()
freebusy
- query
- calendarのlistから空き時間を取得する。
settings
- get
- 指定された設定を取得する。
service.settings().get(setting="settingId").execute()
- list
- 全設定を取得する。
service.settings().list().execute()
- watch
- 設定の変更を監視する。