Microsoft Graph 実務入門 #3 Python SDK 最初の呼び出し — ユーザーと組織データの読み取り
第 2 回でアプリ登録と権限を準備したので、次はコードです。このシリーズは Python を基準の言語にします。社内自動化の現実的な標準で、Python トラックの読者ならそのままつながります(SDK は JavaScript/TypeScript、C#、Java、Go なども同じ構造なので、この回の骨格は他の言語にもそのまま翻訳できます)。今回の目標は、第 4〜7 回で使い回す呼び出しの骨格を作ることです。
インストール — パッケージは二つです #
$ pip install azure-identity msgraph-sdk役割分担が明確です。azure-identity が認証(トークンの取得)を、msgraph-sdk が Graph の呼び出しを受け持ちます。第 2 回で整理した認証フローは、azure-identity ではそれぞれ一つの credential クラスです。device code flow は DeviceCodeCredential、client credentials flow は ClientSecretCredential という形で、概念とコードが 1:1 に対応します。
application クライアント — 無人自動化の骨格 #
まずサーバー自動化の基本形、application(client credentials)側です。第 2 回で作ったテナント ID、クライアント ID、シークレットの三つが材料で、シークレットは環境変数から読みます。
import os
from azure.identity.aio import ClientSecretCredential
from msgraph import GraphServiceClient
def build_app_client() -> GraphServiceClient:
credential = ClientSecretCredential(
tenant_id=os.environ["AZURE_TENANT_ID"],
client_id=os.environ["AZURE_CLIENT_ID"],
client_secret=os.environ["AZURE_CLIENT_SECRET"],
)
# application 権限は個別のスコープの代わりに .default を使います。
# 「アプリ登録に付与(同意)された権限の全部」という意味です。
scopes = ["https://graph.microsoft.com/.default"]
return GraphServiceClient(credential, scopes)見慣れない点が二つあるかもしれません。第一に azure.identity.aio というインポートパスです。Python 用の Graph SDK は async ベースなので、非同期版の credential を使います(後でまた見ます)。第二にスコープが .default 一つという点です。delegated のようにスコープを個別に並べるのではなく、「管理者がこのアプリに同意してくれた application 権限の全部」を意味する固定の文字列です。application 権限の実際の範囲はコードではなくアプリ登録の画面で決まる、という第 2 回の話が、コードにこう反映されます。
delegated クライアント — CLI ツールの骨格 #
人が実行するツールなら delegated です。ターミナルのツールで最も実用的なサインインの方式が device code flow です。コードが URL と使い捨てのコードを表示し、ユーザーがブラウザでサインインするとトークンが発行されます。
import os
from azure.identity.aio import DeviceCodeCredential
from msgraph import GraphServiceClient
def build_user_client() -> GraphServiceClient:
credential = DeviceCodeCredential(
tenant_id=os.environ["AZURE_TENANT_ID"],
client_id=os.environ["AZURE_CLIENT_ID"],
# シークレットがない点に注目 — ユーザーがサインインで証明します
)
# delegated は必要なスコープを明示的に並べます
scopes = ["User.Read", "Mail.Read"]
return GraphServiceClient(credential, scopes)実行するとターミナルに「https://microsoft.com/devicelogin を開いてコード XXXXXXX を入力してください」と出力され、サインインを終えると呼び出しが進みます。delegated ではスコープを明示的に並べること、そしてその権限が「サインインしたユーザーが持つものとの共通部分」だという第 2 回のルールが、ここでもそのままです。
最初の呼び出し — async パターンに慣れる #
骨格ができたので呼び出します。Python SDK のすべての呼び出しは await です。
import asyncio
from graph_app import build_app_client
async def main():
client = build_app_client()
# 組織のユーザー一覧 (application: User.Read.All が必要)
result = await client.users.get()
for user in result.value:
print(user.display_name, "|", user.mail, "|", user.id)
asyncio.run(main())SDK のパスと REST の URL の対応ルールをつかんでおくと、ドキュメントなしでもコードが書けます。URL の各部分がプロパティ・メソッドになり、{id} の位置は by_xxx_id() になります。
# GET /users → client.users.get()
# GET /users/{id} → client.users.by_user_id(uid).get()
# GET /users/{id}/messages → client.users.by_user_id(uid).messages.get()
# GET /me → client.me.get() (delegated 専用)
# POST /users/{id}/sendMail → client.users.by_user_id(uid).send_mail.post(...)me は delegated でだけ意味を持つ点に注意します。application のトークンには「私」がいないので、application のコードでは常に users.by_user_id(...) で対象を明示します。この違いで 403 や 400 にぶつかるのが初心者の定番コースです。
OData を SDK で — クエリパラメータオブジェクト #
第 1 回の OData の文法は、SDK ではクエリパラメータオブジェクトで表現されます。形はやや長いのですが、パターンは一つだけなので、一度テンプレートを作っておいて複製して使います。
from msgraph.generated.users.users_request_builder import UsersRequestBuilder
async def engineering_users(client):
params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=["displayName", "mail", "department"],
filter="department eq 'Engineering'",
top=25,
orderby=["displayName"],
)
config = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=params,
)
return await client.users.get(request_configuration=config)$select を習慣にせよという第 1 回の助言は SDK でも同じです。そして filter やソートを組み合わせて 400 エラー(クエリの組み合わせが未対応)に遭遇したら、コードで格闘する前に同じクエリを Graph Explorer で先に再現するほうがずっと速く済みます。Explorer で完成させてからコードへ移す作業の流れを、改めて強調しておきます。
async が初めてなら — 最小限のルール三つ #
Graph SDK で初めて async に触れる読者のために、必要な最小限だけ押さえます(詳しくは モダン Python 中級 #7 で扱いました)。
- Graph を呼ぶ関数は
async defで宣言し、呼び出しにはawaitを付けます。 - プログラムの入口で
asyncio.run(main())と一度だけ包みます。 awaitを忘れると「coroutine was never awaited」という警告とともに何も起きません。この警告を見たら await の抜けから探します。
バッチスクリプト程度ならこの三つで十分です。async の利点(複数の呼び出しの同時実行)は第 7 回の運用編でまた活用します。
詰まったら — トークンの中身を確認するデバッグ #
第 2 回のエラーパターン(401 は資格情報、403 は権限)に一つ足します。403 が出るのに権限の設定が合って見えるなら、実際のトークンに権限が入っているかを、中身をデコードして確認します。credential の get_token() でトークンの文字列を得て jwt.ms(Microsoft のトークンデコーダ)に貼り付けると、application のトークンなら roles クレームに、delegated のトークンなら scp クレームに権限の一覧が見えます。ここに期待した権限がなければ、問題はコードではなくアプリ登録(同意の欠落)やスコープの指定で、あれば対象リソース側の問題へ方向を絞れます。デバッグ以外の用途でトークンをコピーして保管・共有したりしないことだけ注意します。
まとめ #
- パッケージは azure-identity(認証)と msgraph-sdk(呼び出し)の二つで、第 2 回の認証フローが credential クラスと 1:1 に対応します(無人自動化 = ClientSecretCredential、CLI ツール = DeviceCodeCredential)。
- application はスコープが
.default一つ(実際の範囲はアプリ登録の同意が決定)、delegated はスコープを明示的に並べます。meは delegated 専用です。 - SDK のパスは REST の URL と 1:1 対応です(
{id}→by_xxx_id())。OData はクエリパラメータオブジェクトで表現し、複雑なクエリは Explorer で先に再現します。 - すべての呼び出しは await です。async def、await、asyncio.run の三つのルールでバッチスクリプトには十分です。
- 403 デバッグの最後の手段はトークンの中身を確認することです。roles/scp クレームと期待する権限を突き合わせます。
骨格が完成しました。次回からこの骨格の上に実務のシナリオを載せます。最初は最も需要の大きいメールとカレンダーの自動化です。