Closed11

Google Calendar API with python

alkshmiralkshmir

API の有効化

リンクをクリックして、遷移先画面でGoogle Calendar APIを有効にするプロジェクトを上のメニューから選択

OAuth 同意画面

内部に設定しろと言われているが、Google Workspaceユーザのみ使えると書いてあり、もともとやりたかったことができるのか怪しくなってきた

アプリ名とサポートメールとメールアドレスを入力し、保存して次へを押下

現時点では、スコープの追加をスキップして、[Save and Continue] をクリックします。

テストユーザは設定せずに保存して次へ

デスクトップ アプリケーションの認証情報を承認する

デスクトップアプリケーションではないのでサービスアカウントでいい気がするが、とりあえず指示に従ってOAuthクライアントIDを作成

デスクトップOAuthクライアントを作成した

JSONをダウンロードを押下し、credentials.jsonにリネーム

alkshmiralkshmir
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が出力されたのでこれをブラウザで開く

うん、そうだね

おそらく、自分のアカウントをテスターに追加する必要がある

alkshmiralkshmir

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.

alkshmiralkshmir

大昔にgmailのAPIを叩いたときはoauth2libを使っていたけど、非推奨になったらしい
今はgoogle-authライブラリを使うようにするらしい。

それでこのように認可コードフローが透けて見える感じのコードになっている模様。

alkshmiralkshmir

サービスアカウントと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.

https://docs.gspread.org/en/v6.0.0/oauth2.html#authentication

ということのよう。
もともとやりたかったのはボットとしてアクセスすることだったので、やはりサービスアカウントが適している。
というわけでサービスアカウントのcredentialでGoogle Calendarを操作できるのか見てみたい

でもこれってgspreadのような抽象化レイヤーがない場合ややめんどくさそう

alkshmiralkshmir

サービスアカウントの作成・認証情報の取得

もう作っていたので省略。
鍵の作成時に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の場合と同じ結果を得られた。

alkshmiralkshmir

イベント作成

https://developers.google.com/calendar/api/guides/create-events?hl=ja#python

ここに書いてあることを試す

いろいろオプションが書いてあったが、いまは要らなそうなのでとりあえずシンプルな例で試す

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 カレンダーの予定と同期できます。また、カレンダーのバックエンドで正常に実行された後、ある時点でオペレーションが失敗しても、予定が重複して作成されることを防ぎます。

alkshmiralkshmir

ID 指定

ID に使用できる文字は、base32hex エンコードで使用されている文字です。つまり、小文字の a ~ v と数字の 0 ~ 9 を使用します。RFC2938 のセクション 3.1.2 をご覧ください。
ID の長さは 5 ~ 1,024 文字にする必要があります
ID はカレンダーごとに一意である必要があります。
システムはグローバルに分散しているため、イベント作成時に ID の競合が検出されるとは限りません。衝突のリスクを最小限に抑えるため、RFC4122 で記述されているような確立された UUID アルゴリズムを使用することをおすすめします。

https://developers.google.com/calendar/api/v3/reference/events?hl=ja#id

ペイロードに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という例外が返ってきた

このスクラップは2024/05/02にクローズされました