🥳

PythonからSharePointにファイルアップロードしたいッッ!

2023/10/19に公開2

記事を書いた背景

企業内で業務自動化システムを作成するときに、Microsoft Teams(以下、Teams)やMicrosoft SharePoint(以下、SharePoint),Microsoft OneDriveに対する処理を自動化したいというニーズは意外と存在する。
単純にTeamsに通知したい程度のユースケースであればpymsteams等のライブラリの利用だけで完結するが、TeamsやSharePoint,OneDrive等にPythonからファイルを自動アップロードしたい等のユースケースにおいてはMicrosoft Graph APIの利用が必要になってくるため、ハードルが一気に上がる。
自分自身も調べていて難しいなと感じた部分もあるため、備忘録も兼ねてMicrosoft Graph APIを利用したPythonによるファイルの自動アップロードのやり方を記しておく。

この記事の想定読者

  • PythonでMicrosoftサービスを操作するような自動化システムを作りたい人
  • Microsoft Graph APIを触ってみようとして挫折した人
  • Pythonに関する基礎的な知識を持っている人
  • 最低限のHTTPHandler通信についての知識を持っている人

動作環境

当記事で提示しているコードは以下の環境で動作確認済み。

Microsoft Graph APIとは

Microsoft Graph は、Microsoft 365 のデータとインテリジェンスへの入り口です。 Microsoft Graph は、Microsoft 365、Windows、および Enterprise Mobility + Security の膨大な量のデータにアクセスする際に使用できる統合型プログラミング モデルを提供します。 Microsoft Graph の豊富なデータを使用して、数百万人のユーザーを操作する組織やコンシューマー向けのアプリを作成できます。

簡単にまとめると、Microsoftが提供している様々なクラウドサービスを、単一のAPIから操作できるようにしたインターフェースということである。

https://learn.microsoft.com/ja-jp/graph/overview

そもそもAPIとは?

そもそもAPIと言われてピンと来ない人もいるかも知れないので、APIについて軽く説明しておく。
APIとはApplication Programming Interfaceの略称で、システムやプログラム、Webサービス同士を統一したルールに基づいて接続するためのインターフェース(接点)のことを指す。
特に今回扱うMicrosoft Graph APIは、読者の皆さんが作成している自動化システムと自組織のMicrosoftサービスを接続するために提供されているインターフェースである。
Microsoft Graph APIは、Pythonのrequestsライブラリを用いることで、HTTP(HTTPS)通信を通して接続することができる。
APIのアクセスを受け付けているPCのことをAPIサーバーという。

セキュリティについて

先に述べた通り、APIと呼ばれるインターフェースにHTTP(HTTPS)通信を行うことで、組織のMicrosoftサービスの情報を取得したり、新しくリソースを作成したりということが可能になる。
そのため、APIの利用にはセキュリティに関する詳細な設定を予め行っておく必要がある。Microsoft Graph APIでは、OAuth2と呼ばれる認証プロトコルを使用している。
具体的には、以下のようなフローで認証を行った上で、Microsoftサービスに対する操作を行えるようになる。

API利用手順概要

Microsoft Graph APIを利用してSharePointに対してファイルアップロードを行うための手順を以下に提示する。

  1. 🔐Azure Entra IDの設定画面にアクセスする
  2. 🔐アプリケーションの登録タブを選択する
  3. 🔐アプリケーションを新規登録する
  4. 🔐概要ページから必要な情報を取得する
  5. 🔐証明書とシークレットページでクライアントシークレットを作成する
  6. 🔐APIのアクセス許可ページで適切な権限を付与し、管理者の同意を与える
  7. parameters.jsonファイルを作成する
  8. 必要なライブラリのインストール
  9. Pythonでアクセストークンを取得する
  10. requestsライブラリでAPIアクセス用関数を定義する
  11. Microsoft Graph APIのエンドポイント設計について理解する
  12. SharePoint上の特定のディレクトリにファイルをアップロードする

1. 🔐Azure Entra IDの設定画面にアクセスする

Azure Portalから、Azure Entra ID (旧称: Azure Active Directory)の設定画面にアクセスする。
Azure Entra IDの設定画面リンク

2. 🔐アプリケーションの登録タブを選択する

左側のサイドバーから「アプリケーションの登録」タブを選択する

3. 🔐アプリケーションを新規登録する

画面中央上部の「+新規登録」ボタンを押す。

遷移後の登録画面で任意のアプリケーション名をつける。
その他は何も触らず、画面下部の「登録」ボタンを押す。

4. 🔐概要ページから必要な情報を取得する

左側のサイドバーで「概要」ページが開かれていることを確認する。
PythonからMicrosoft Graph APIにアクセスする際に必要な以下の情報を何処かにメモしておく。

  • アプリケーション(クライアント)ID
  • ディレクトリ(テナント)ID

5. 🔐証明書とシークレットページでクライアントシークレットを作成する

左側のサイドバーから「証明書とシークレット」ページを開く。
画面中央左の「+新しいクライアントシークレット」ボタンを押す。
画面右側に出現するサイドスクリーンにてクライアントシークレットの説明を入力し、必要に応じて有効期限を変更する。(特に理由がない人は「推奨: 180日(6か月)」のままで良い)
サイドスクリーン下部の「追加」ボタンを押す。

作成されたクライアントシークレットの「値」の方をコピーしてメモしておく。

6. 🔐APIのアクセス許可ページで適切な権限を付与し、管理者の同意を与える

PythonからSharePointにファイルをアップロードするためのアプリケーションに必要なアクセス許可の最小構成は以下の通り。

  • Files.ReadWrite.All
  • Sites.Read.All

左側のサイドバーから「APIのアクセス許可」ページを開く。
画面中央左の「+アクセス許可の追加」ボタンを押す。
画面右側に出現するサイドスクリーンにて「Microsoft Graph」を選択する。

「アプリケーションの許可」を選択。

検索窓で「Files」と検索し、Filesタブ内のFiles.ReadWrite.Allにチェックを入れる。

同様に、「Sites」と検索し、Sitesタブ内のSites.Read.Allにチェックを入れる。
サイドスクリーン下部の「アクセス許可の追加」ボタンを押す。

ここで追加した2つのアクセス許可は明示的な管理者の同意を必要とする。
画面中央の「✓{組織}に管理者の同意を与えます」ボタンを押す。

ポップアップの「はい」を選択する。

先程追加した2つのアクセス許可の状態が「✅{組織}に付与されました」となっていれば成功。

7. parameters.jsonファイルを作成する

Azire Entra IDでの操作で取得できたclient_id, tenant_id, client_secretの値を使用し、parameters.jsonという名前でjsonファイルを作成する。

parameters.json
{
    "authority": "https://login.microsoftonline.com/YOUR_TENANT_ID",
    "client_id": "YOUR_CLIENT_ID",
    "scope": [ "https://graph.microsoft.com/.default" ],
    "secret": "YOUR_CLIENT_SECRET"
}

8. 必要なライブラリのインストール

以下のコマンドで必要なライブラリをpip installする。

pip
 pip install requests==2.31.0 msal==1.24.1

main.pyを作成し、以下のimport文を記述する。

main.py
import json
import logging
import requests
import msal

9. Pythonでアクセストークンを取得する

先程作成したconfig.jsonを読み込み、msalライブラリのConfidentialClientApplicationクラスをインスタンス化する。

main.py
# config.jsonの読み込み
config = json.load(open("parameters.json"))

# msal.ConfidentialClientApplicationのインスタンス化
app = msal.ConfidentialClientApplication(
    config["client_id"], authority=config["authority"],
    client_credential=config["secret"],
)

以下のコードで、アクセストークンを含んだ辞書型の値が取得できる。

main.py
def get_access_token() -> dict:
    # アクセストークンの取得
    result = app.acquire_token_silent(config["scope"], account=None)

    # アクセストークンが取得できなかった場合は、AADから取得する
    if not result:
        logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
        result = app.acquire_token_for_client(scopes=config["scope"])

    return result

10. requestsライブラリでAPIアクセス用関数を定義する

Microsoft Graph APIの特定のエンドポイントに対してGETリクエストを飛ばせる関数を定義する。

main.py
def get_some_data_from_graph_api(endpoint: str) -> None:
    # アクセストークンの取得
    access_token = get_access_token()

    if "access_token" in access_token:
        graph_data = requests.get(
            endpoint,
            headers={'Authorization': 'Bearer ' + access_token['access_token']}, ).json()
        return graph_data
    else:
        print(access_token.get("error"))
        print(access_token.get("error_description"))
        print(access_token.get("correlation_id"))
        raise Exception("===Failed to call MS Graph API. See stderr for details.===")

Microsoft Graph APIの特定のエンドポイントに対してファイルをアップロードできる関数を定義する。

main.py
def put_file_to_graph_api(endpoint: str, file_path: str) -> None:
    # アクセストークンの取得
    access_token = get_access_token()

    if "access_token" in access_token:
        with open(file_path, 'rb') as f:
            graph_data = requests.put(
                endpoint,
                headers={'Authorization': 'Bearer ' + access_token['access_token']},
                data=f).json()
        return graph_data
    else:
        print(access_token.get("error"))
        print(access_token.get("error_description"))
        print(access_token.get("correlation_id"))
        raise Exception("===Failed to call MS Graph API. See stderr for details.===")

11. Microsoft Graph APIのエンドポイント設計について理解する

エンドポイントとは

APIサーバーがデパートだとすると、エンドポイントはデパートに出店している各テナント店舗のようなイメージである。

https://graph.microsoft.com/v1.0/sites

というURLがあったとき、graph.microsoft.comの部分がドメイン(=デパートの住所)であり、/v1.0/sitesの部分がパス(=デパート内の店の構造)である。

例えるならば上のURLは、graph.microsoft.com(=Graphデパート)のv1.0(=1階)にあるsites(=店名)にアクセスするという意味になる。

Microsoft Graph APIでは、Graphデパート1階のsitesという店に来ると、その組織の全てのサイトの一覧情報を取得することができるように設計されている。
その際、必要な権限を満たしたアクセストークンを持っていない場合は門前払いを食らってしまうため、適切なアクセストークンと共にリクエストを送る必要がある。(403エラー)

また、どのパスにどのようにアクセスしたら何の機能が使えるのかの説明書であるMicrosoft Graph API エンドポイントリファレンスがMicrosoftから公式提供されているため、SharePointへのアップロード以外になにができるのか気になる人は以下リンクから一読してみるといいだろう。

https://learn.microsoft.com/ja-jp/graph/api/overview?view=graph-rest-1.0

ファイルアップロード用エンドポイント

今回の記事の目的である「SharePointへのファイルアップロード」の機能をサポートしているエンドポイントは、以下のようなURL構造になっている。

PUT /sites/{site-id}/drive/items/{parent-id}:/{filename}:/content

このエンドポイントへのアクセスには、以下のうちいずれかの権限を有したアクセストークンを保持している必要がある。(下に行くほど大きい権限)

  • Files.Read.All
  • Files.ReadWrite.All

各パラメータの説明は以下の通り。

パラメータ 説明
site-id ファイルをアップロードしたいディレクトリがあるサイトのid
parent-id ファイルをアップロードしたいディレクトリのid
filename アップロード後のファイル名

つまりAPI経由でファイルアップロードをするためには、アップロード対象のsite-idparent-idを取得しておく必要がある。

https://learn.microsoft.com/ja-jp/graph/api/driveitem-put-content?view=graph-rest-1.0&tabs=http#to-upload-a-new-file

https://qiita.com/tamikura@github/items/d101757fe04dfc698c00#:~:text=サイトは階層構造で,的な差はありません。

組織に存在するアクセス可能なサイトの情報を取得するエンドポイント

組織に存在するアクセス可能な全てのサイトの情報を取得するために、以下のエンドポイントが利用できる。

GET /sites

このエンドポイントへのアクセスには、以下のうちいずれかの権限を有したアクセストークンを保持している必要がある。(下に行くほど大きい権限)

  • Sites.Read.All
  • Sites.ReadWrite.All

つまり、https://graph.microsoft.com/v1.0/sitesにアクセスすると、組織に存在するサイトの中でアクセス可能なものの情報の一覧が取得されるということだ。
先程定義したAPIに対してGETリクエストを飛ばせる関数を用いて以下のように書くことで、このエンドポイントにアクセスすることができる。

main.py
sites = get_some_data_from_graph_api("https://graph.microsoft.com/v1.0/sites")
print(sites)

プログラムを実行して大きめのjsonが返ってきた方はおそらく成功している。
(※レスポンスのjsonを載せたかったが、ほぼ全てがモザイクになってしまうため割愛)
最終的に欲しいのはsite-idなので、数あるサイトの中から今回はgeneralサイトのidを取得するコードを書いてみよう。

main.py
# 例: generalサイトのIDを取得する
sites = get_some_data_from_graph_api("https://graph.microsoft.com/v1.0/sites")
target_site_name = "general"
target_site = None
for site in sites["value"]:
    try:
        if site["displayName"] == target_site_name:
            target_site = site
    except KeyError:
        pass
target_site_id = target_site["id"]
print(target_site_id)

https://learn.microsoft.com/ja-jp/graph/api/site-list?view=graph-rest-1.0&tabs=http#http-request

特定のサイトのドライブ内のディレクトリ情報を取得するエンドポイント

特定のサイトに存在するディレクトリの情報を取得するために、以下のエンドポイントが利用できる。

GET /sites/{site-id}/drive/items/{item-id}/children

このエンドポイントへのアクセスには、以下のうちいずれかの権限を有したアクセストークンを保持している必要がある。(下に行くほど大きい権限)

  • Sites.Read.All
  • Sites.ReadWrite.All

先程取得したsite-idを用いて以下のように書くことで、generalサイトのドライブのrootディレクトリの子要素の情報が取得できる。

main.py
exp_dir_id = get_some_data_from_graph_api("https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/root/children")
print(exp_dir_id)

こちらもプログラムを実行して大きめのjsonが返ってきた方はおそらく成功している。
最終的に欲しいのはparent-id(ファイルをアップロードしたいディレクトリのid)なので、数あるディレクトリの中から今回はrootディレクトリ直下に作成した「実験用」というディレクトリのidを取得するコードを書いてみよう。

main.py
# 例: generalサイトのドライブ内のGraphTestフォルダのIDを取得する
children = get_some_data_from_graph_api(f"https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/root/children")
exp_dir_name = "実験用"
exp_dir = [child for child in children["value"] if child["name"] == exp_dir_name][0]
exp_dir_id = exp_dir["id"]
print(exp_dir_id)

https://learn.microsoft.com/ja-jp/graph/api/driveitem-list-children?view=graph-rest-1.0&tabs=http#http-request

12. SharePoint上の特定のディレクトリにファイルをアップロードする

ファイルアップロードのエンドポイントを叩くのに必要なsite-idparent-idを取得することに成功したので、いよいよ本題のファイルアップロード処理を実装していく。
叩くべきエンドポイントは先程示したが、念のためもう一度提示しておく。

PUT /sites/{site-id}/drive/items/{parent-id}:/{filename}:/content

先程取得したsite-idとparent-id、そしてアップロード後のファイル名をfilenameに当てはめたURLと、だいぶ前に定義したファイルアップロード用関数を使用してアクセス処理を書いたものが以下の通り。

main.py
filename = "hogehoge.txt
filepath = "./hogehoge.txt"

resp = put_file_to_graph_api(f"https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{exp_dir_id}:/{filename}:/content", filepath)
print(resp)

こちらも大きめのjsonが返ってきたら成功している。
このプログラムを実行した直後に実際にSharePointのGeneralサイトの実験用ディレクトリを見てみると、ちゃんとSharePointアプリからhogehoge.txtが無事アップロードされていることがわかる。

余談だが、レスポンスjson内の"@microsoft.graph.downloadUrl"というキーの値に入っているURLはアップロードしたファイルのダウンロード用URLなので、ファイルアップロード後はこのURLをシェアすると、そのリンクから全員が簡単にファイルをダウンロードすることができたりする。

まとめ

この記事では、PythonからSharePointにファイルをアップロードするためのAzure Portal上での設定方法から、Pythonからエンドポイントを叩いて実際にファイルをアップロードするコードの実装部分までを解説してきた。
セクション10で定義した「GETリクエストを飛ばす関数」や「ファイルをアップロードする関数」について、なぜこれで通信ができるのかという点などのPythonコード自体の細かい点についてはあまり触れなかったが、興味のある方はGPT等を活用して調べてみて欲しい。

出典

https://learn.microsoft.com/ja-jp/azure/active-directory/develop/quickstart-daemon-app-python-acquire-token
https://learn.microsoft.com/ja-jp/graph/overview
https://learn.microsoft.com/ja-jp/graph/api/overview?view=graph-rest-1.0
https://learn.microsoft.com/ja-jp/graph/api/driveitem-put-content?view=graph-rest-1.0&tabs=http#to-upload-a-new-file
https://qiita.com/tamikura@github/items/d101757fe04dfc698c00#:~:text=サイトは階層構造で,的な差はありません。
https://learn.microsoft.com/ja-jp/graph/api/site-list?view=graph-rest-1.0&tabs=http#http-request
https://learn.microsoft.com/ja-jp/graph/api/driveitem-list-children?view=graph-rest-1.0&tabs=http#http-request

Discussion

ぬっこぬっこ

大変参考になる解説をありがとうございます。同じ処理を行いたく検証していたところ次の箇所でエラーに遭遇してしまいました

'組織に存在するアクセス可能なサイトの情報を取得するエンドポイント'の解説の箇所で
target_site_name = "general"
とありましたので私のテナントで、例として
testtop
と値を設定したところ、おそらくそんなサイトはないというエラーが返された状況です
この、target_site_nameというのはどの値なのか、ご教示願えますと幸いです

でんちゅーでんちゅー

コメントありがとうございます!

テナントが存在しないというエラーが発生したとのことですが、記事中の組織に存在するアクセス可能なサイトの情報を取得するエンドポイントというステップで、以下のコードを実行してみてください。

# 例: アクセス可能な全てのサイトのサイト名とid一覧を取得する
sites = get_some_data_from_graph_api("https://graph.microsoft.com/v1.0/sites")
for site in sites["value"]:
    print(f"{site['displayName']}: {site['id']}")

ここにtesttopというテナント名が表示されなかった場合、権限周りの問題の可能性がありますのでまたご連絡ください。