Microsoft Graph 実務入門 #3 Python SDK 最初の呼び出し — ユーザーと組織データの読み取り

読了 6分

第 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、シークレットの三つが材料で、シークレットは環境変数から読みます。

graph_app.py — application 権限のクライアント
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 と使い捨てのコードを表示し、ユーザーがブラウザでサインインするとトークンが発行されます。

graph_user.py — delegated 権限のクライアント
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 です。

main.py — 最初の呼び出し
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() になります。

URL → SDK の対応
# 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 ではクエリパラメータオブジェクトで表現されます。形はやや長いのですが、パターンは一つだけなので、一度テンプレートを作っておいて複製して使います。

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 で扱いました)。

  1. Graph を呼ぶ関数は async def で宣言し、呼び出しには await を付けます。
  2. プログラムの入口で asyncio.run(main()) と一度だけ包みます。
  3. 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 クレームと期待する権限を突き合わせます。

骨格が完成しました。次回からこの骨格の上に実務のシナリオを載せます。最初は最も需要の大きいメールとカレンダーの自動化です。

X