Google Calendar APIをPythonで叩く

昔作ったGoogle Calendar Event登録ScriptをPythonで全面的に書き換えるべく作業を始めようと思ったはいいがもう何年も前なので完全にやり方を忘れた。またどうせ忘れるのでここにメモしておこうと思う。

大まかな流れは以下のようになる。

  1. プロジェクトの作成
  2. 認証情報を作成
  3. 認証情報を使って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の取得すらできない。

準備としてまず

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 CalendarAPIを叩いてるだけだった。という事は上記の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する。

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()

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の変更を監視する。

freebusy

query
calendarのlistから空き時間を取得する。

settings

get
指定された設定を取得する。

service.settings().get(setting="settingId").execute()

list
全設定を取得する。

service.settings().list().execute()

watch
設定の変更を監視する。