Google Calendar API with python
これをやってみる
API の有効化
リンクをクリックして、遷移先画面でGoogle Calendar APIを有効にするプロジェクトを上のメニューから選択
OAuth 同意画面
内部に設定しろと言われているが、Google Workspaceユーザのみ使えると書いてあり、もともとやりたかったことができるのか怪しくなってきた
アプリ名とサポートメールとメールアドレスを入力し、保存して次へを押下
現時点では、スコープの追加をスキップして、[Save and Continue] をクリックします。
テストユーザは設定せずに保存して次へ
デスクトップ アプリケーションの認証情報を承認する
デスクトップアプリケーションではないのでサービスアカウントでいい気がするが、とりあえず指示に従ってOAuthクライアントIDを作成
デスクトップOAuthクライアントを作成した
JSONをダウンロードを押下し、credentials.json
にリネーム
python -m venv venv
source venv/bin/activate
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
quickstart.pyを作成する
python quickstart.py
承認を求められると書いてあるが何も起きない。WSLだから?
サンプルプログラムで
creds = flow.run_local_server(port=0)
こうかいてあるところを
creds = flow.run_local_server(open_browser=False)
これにしたらURLが出力されたのでこれをブラウザで開く
うん、そうだね
おそらく、自分のアカウントをテスターに追加する必要がある
OAuth同意画面を再度開いてテストユーザの項目でADD USERSを押下、自分のgmailアドレスを入力した
再度quickstart.py
を実行して出力されたURLをブラウザで開く
このアプリはGoogleで確認されていません
とでるが、続行を押下
<アプリ名> が Google アカウントへのアクセスを求めています
と表示されるので続行を押下
したが、タイムアウトしてしまった。
ブラウザ表示を見るに、localhost:8080
につなごうとしているっぽい。
Traceback (most recent call last):
File "/home/shira/work/calendar-test/quickstart.py", line 70, in <module>
main()
File "/home/shira/work/calendar-test/quickstart.py", line 32, in main
creds = flow.run_local_server(open_browser=False)
File "/home/shira/bin/.pyenv/versions/3.10.12/lib/python3.10/site-packages/google_auth_oauthlib/flow.py", line 476, in run_local_server
authorization_response = wsgi_app.last_request_uri.replace("http", "https")
AttributeError: 'NoneType' object has no attribute 'replace'
まじでよくわからないけどもう一回やったら動いた。
The authentication flow has completed. You may close this window.
大昔にgmailのAPIを叩いたときはoauth2libを使っていたけど、非推奨になったらしい。
今はgoogle-auth
ライブラリを使うようにするらしい。
それでこのように認可コードフローが透けて見える感じのコードになっている模様。
サービスアカウントとOAuthの区別があいまいだったが、
If you plan to access spreadsheets on behalf of a bot account use Service Account.
If you'd like to access spreadsheets on behalf of end users (including yourself) use OAuth Client ID.
ということのよう。
もともとやりたかったのはボットとしてアクセスすることだったので、やはりサービスアカウントが適している。
というわけでサービスアカウントのcredentialでGoogle Calendarを操作できるのか見てみたい
でもこれってgspreadのような抽象化レイヤーがない場合ややめんどくさそう
サービスアカウントの作成・認証情報の取得
もう作っていたので省略。
鍵の作成時にJSONでサービスアカウントの認証情報をダウンロードできる。
{
"type": "service_account",
"project_id": "xxx",
"private_key_id": "xxx",
"private_key": "-----BEGIN PRIVATE KEY-----xxx-----END PRIVATE KEY-----\n",
"client_email": "xxx@xxx.iam.gserviceaccount.com",
"client_id": "xxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/hogehoge"
}
client_email
がサービスアカウントのemailアドレスである
creds.json
という名前でサンプルプログラムと同じディレクトリに保存した
サービスアカウントとカレンダーを共有
普通のGoogleアカウントにカレンダーを共有するときと全く同じ
Google Calendar -> マイカレンダー -> 設定と共有 -> 特定のユーザーまたはグループと共有する
client_email
に書いてあるアドレスを貼り付けて、必要な権限を付与(今回は「予定の変更」にした)
カレンダーIDを確認
「カレンダーの統合」欄に書いてある。今回は、自分のGmailアドレスだった。
サンプルプログラム
サンプルプログラムを次のように書き換えた
import datetime
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/calendar']
SERVICE_ACCOUNT_FILE = 'creds.json'
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
calendar = build("calendar", "v3", credentials=credentials)
now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time
print("Getting the upcoming 10 events")
events_result = (
calendar.events()
.list(
calendarId="CALENDAR_ID",
timeMin=now,
maxResults=10,
singleEvents=True,
orderBy="startTime",
)
.execute()
)
events = events_result.get("items", [])
for event in events:
start = event["start"].get("dateTime", event["start"].get("date"))
print(start, event["summary"])
まず、サービスアカウントの認証情報を作成する。
SCOPES = ['https://www.googleapis.com/auth/calendar']
SERVICE_ACCOUNT_FILE = 'creds.json'
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
そのほかはほとんど同じだが、calendarID
を先ほど確認したIDへ変更すること。
events_result = (
calendar.events()
.list(
calendarId="CALENDAR_ID",
timeMin=now,
maxResults=10,
singleEvents=True,
orderBy="startTime",
)
.execute()
)
これがprimary
の場合、ログインしているユーザのメインカレンダーへアクセスする。
サービスアカウントにはカレンダーが紐づいていないので、primary
のままでは何も表示されない。
共有設定が適切になされている場合、calendarID
を指定するだけでアクセスできた。
OAuth2の場合と同じ結果を得られた。
イベント作成
ここに書いてあることを試す
いろいろオプションが書いてあったが、いまは要らなそうなのでとりあえずシンプルな例で試す
import datetime
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/calendar']
SERVICE_ACCOUNT_FILE = 'creds.json'
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
calendar = build("calendar", "v3", credentials=credentials)
new_event = {
'summary': 'Test event',
'location': 'test location',
'description': 'longer description',
'start': {
'dateTime': '2024-05-02T07:00:00+09:00'
},
'end': {
'dateTime': '2024-05-02T08:00:00+09:00',
}
}
new_event = calendar.events().insert(calendarId='CALENDER_ID', body=new_event).execute()
print('Event created: %s' % (new_event.get('htmlLink')))
- 例によって
CALENDAR_ID
を置き換えること
実行するとURLが出力され、新しいイベントが作成されていることが確認できた。
❯ python event_create.py
Event created: https://www.google.com/calendar/event?eid=xxxxxxx
なお、2回実行すると同じ時間に同じイベントが作成されるっぽく、排他制御しないといけない?
あるいは、IDを指定して作成する方法もある模様
イベントを作成する際に、独自のイベント ID を生成できます。
フォーマットの要件に準拠する必要がありますこれにより、ローカル データベースのエンティティを Google カレンダーの予定と同期できます。また、カレンダーのバックエンドで正常に実行された後、ある時点でオペレーションが失敗しても、予定が重複して作成されることを防ぎます。
ID 指定
ID に使用できる文字は、base32hex エンコードで使用されている文字です。つまり、小文字の a ~ v と数字の 0 ~ 9 を使用します。RFC2938 のセクション 3.1.2 をご覧ください。
ID の長さは 5 ~ 1,024 文字にする必要があります
ID はカレンダーごとに一意である必要があります。
システムはグローバルに分散しているため、イベント作成時に ID の競合が検出されるとは限りません。衝突のリスクを最小限に抑えるため、RFC4122 で記述されているような確立された UUID アルゴリズムを使用することをおすすめします。
ペイロードにid
を指定したら2回目以降は例外になるようになった。
new_event = {
'id': 'testid1234',
'summary': 'Test event',
'location': 'test location',
'description': 'longer description',
'start': {
'dateTime': '2024-05-02T07:00:00+09:00'
},
'end': {
'dateTime': '2024-05-02T08:00:00+09:00',
}
}
2回目はHttpError 409という例外が返ってきた
だいたい分かったのでクローズ
API ドキュメント