チュートリアル / Shotgunスクリプト入門
第2回:Slackと連携してみよう
- Flow Production Tracking
- コラム
- スクリプト・API
- スマイルテクノロジーユナイテッド
- チュートリアル
- 上級者
- 中級者
はじめに
皆さんこんにちは。スマイルテクノロジーユナイテッド株式会社の榎本です。
今回は、前回に引き続き、ファイルアップローダーを題材に、Shotgun スクリプトに触れていただきたいと思います。
1. 今回の内容
皆さんの現場ではチャットサービスを使われているでしょうか。近年、Slack や Chatwork を始めとするチャットサービスの普及が進んでいると思います。弊社でも Slack をメインに利用しており、すでに業務に欠かせないものになっています。
チャットサービスの利用が進むほど、さまざまなメッセージをチャットで受け取りたくなります。Shotgun からの通知はメールで受けられますが、普段のやりとりに使われているチャットサービスとの連携ができれば、Shotgun のさらなる活用が期待できます。
今回は、前回の続きとしてファイルアップローダーの改良と、アップローダーと Slack との連携の二つについてやっていきます。
Slackとの連携方法の種類
Shotgun と Slack の連携はさまざまな手段が考えられますが、大きく分けて二つの方法があります。
<Shotgun と Slack の連携方法>
1. ファイルアップローダーの機能として実装する
2. Shotgun イベントデーモン(*1)として実装する
それぞれの違いについて図式化すると以下のようになりますが、連携方法によって特徴があります。
<連携方法による特徴>
1. ファイルアップローダーの機能としての実装
◯ アプリケーション内に機能追加するだけで済むため実装がシンプル
× Shotgun との連携はアプリケーションの実装次第になる
2. Shotgunイベントデーモンとしての実装
◯ Shotgun の情報更新に合わせたメッセージ通知が可能
× Shotgun イベントデーモンを稼働させるサーバが必要となり実装がやや複雑になる
今回の連携方法としてはファイルアップローダーに Slack への通知機能を実装する方法をとります。
*1 Shotgun 開発ガイド: https://support.shotgunsoftware.com/hc/ja/articles/219031298
ファイルアップローダーの改良
前回実装した、バージョンへのファイルリンク作成だけでは、十分に実用的ではありません。例えば、Shotgun では通常、バージョンはアセットやショット等に関連付けて作成します。前回の仕様では、手作業でバージョンをアセットへリンクする必要があります。また、同名のバージョンが複数存在すると、どれが最新データを持っているかを確認する手間が発生するかもしれません。
TA としては、こうした手間の掛かる作業を解消したいところです。
他にも、アップロードが完了した際に実行結果を表示すると親切でしょう。
今回は上記の例に則って機能を追加していくことにします。追加実装していく機能をまとめると、下記の三つになります。
● バージョン作成と同時にアセットを作成し、バージョン・アセット間のリンクを作成する。
● すでに同名のバージョンが存在するかチェックし、なければ作成する。既にある場合は、同名のバージョンの重複チェックを行い、一意であればファイルリンクを上書きし、重複があった場合はアップロードをせずスキップする。
● 一連のアップロードが終わったタイミングで、アップロードに成功したファイルとスキップしたファイルのファイル名をまとめて表示する。
ファイルアップローダーと Slack の連携
ファイルアップローダーで大量のファイルをアップロードした時に、完了報告を受け取りたいということがあるかと思います。
そこで今回は、チャットアプリの Slack と連携し、Slack の所定のチャンネルに完了報告のメッセージを送信するようにします。
Slack へメッセージを送る方法は、Shotgun スクリプトのようにトークンを利用する方法と、Incoming Webhook という仕組みを利用する方法があります。今回は Incoming Webhook を利用してメッセージを Slack へ送るようにしていきます。
2. ファイルアップローダーの改良
2.1. ツールの構成
今回のツールの構成を下図に示します。
myuploader ├ cli.py ├ myuploader.bat │ ├ config │ └ config.json │ └ images ├ 001.png ├ 002.png . └ 100.png
前回からの変更点としては、myuploader/config/config.json(以下、config.json)の追加のみです。
スクリプトが長くなってくると、トークンなどの重要なデータをどこに記述したか探しにくくなるため、Shotgun の設定を別のファイルに JSON 形式(*2)で分離し、そこから読み込んでくるようにします。今回は、config.jsonに分離します。後程登場する、Slack の Incoming Webhook の URL もここに併せて保存します。
*2 JSON: https://www.json.org/json-ja.html
2.2. コーディング
config.json を下記のように記述します。後で Slack の設定も追加するため、”shotgun” 以下にまとめておきます。
{ "shotgun": { "target_dir": "images", "url": "https://smiletechnologyunited.shotgunstudio.com", "script_name": "myuploader", "api_key": "fouetfj8uebo3bjflneghqorp", "project_id": 90 } }
cli.py を下記のように編集します。
import json import os import sys from shotgun_api3 import Shotgun class IdentityError(Exception): pass def base_dir(): """このファイルが置かれているフォルダのパスを返す :rtype: str """ return os.path.dirname(os.path.abspath(__file__)) def config_path(): """config.jsonファイルのパスを返す :rtype: str """ return os.path.join(base_dir(), "config", "config.json") def get_config(): """config.jsonの中身を取得して返す :rtype: dict """ with open(config_path(), "r") as fp: config = json.load(fp) return config def find_unique(shotgun, entity_type, filters, fields): """一意のエンティティを取得する 対象エンティティが複数存在したらIdentityErrorを送出する :type shotgun: shotgun_api3.Shotgun :type entity_type: str :type filters: list :type fields: list :rtype: dict """ _entities = shotgun.find(entity_type, filters, fields) if not _entities: return None elif len(_entities) == 1: entity = _entities[0] return entity else: raise IdentityError def main(): """メイン関数 :rtype: int """ config = get_config() # Shotgunの設定 shotgun_config = config["shotgun"] target_dir = shotgun_config["target_dir"] url = shotgun_config["url"] script_name = shotgun_config["script_name"] api_key = shotgun_config["api_key"] project_id = shotgun_config["project_id"] project = {"type": "Project", "id": project_id} # Shotgunインスタンスの取得 shotgun = Shotgun(url, script_name=script_name, api_key=api_key) # 対象のエンティティをバージョン、アップロード先のフィールドをsg_uploaded_movieとします entity_type = "Version" field_code = "sg_uploaded_movie" error_list = [] target_list = os.listdir(target_dir) for _file in target_list: try: filters = [["project", "is", project], ["code", "is", _file]] fields = ["id"] version = find_unique(shotgun, entity_type, filters, fields) except IdentityError: error_list.append(_file) continue if not version: # ファイル名と同じ名前のバージョン名と、所属するプロジェクトの情報を持たせます entity_data = {"code": _file, "project": project} # バージョンエンティティを作成します version = shotgun.create(entity_type, entity_data) # versionにはどのようなデータが入っているでしょうか? print(version) # 作成したバージョンのidを取得します version_id = version["id"] file_path = os.path.join(target_dir, _file) # 作成したバージョンにファイルをアップロードします shotgun.upload( entity_type, version_id, file_path, field_name=field_code, display_name=_file ) # Assetの名前は、ファイル名から拡張子を除いたものにします name, ext = os.path.splitext(_file) asset = find_unique( shotgun, "Asset", [["project", "is", project], ["code", "is", name]], ["id"] ) if not asset: asset_entity_data = {"code": name, "project": project} asset = shotgun.create("Asset", asset_entity_data) # 作成されたAssetの情報を確認します print(asset) # 新規作成時にはリンクを作成できないので、アップデートで作成します asset_id = asset["id"] link_data = {"sg_versions": [{"type": "Version", "id": version_id}]} shotgun.update("Asset", asset_id, link_data) print("") uploaded_list = sorted(list(set(target_list).difference(set(error_list)))) if uploaded_list: print("アップロードされたファイル: {0}".format(len(uploaded_list))) for _file_name in uploaded_list: print(_file_name) print("") if error_list: print("重複しているファイル: {0}".format(len(error_list))) for _error_message in error_list: print(_error_message) return 0 if __name__ == "__main__": sys.exit(main())
前回からは、main 関数以外に base_dir、config_path、get_config、find_unique 関数と IdentityError 例外クラスを追加しています。
base_dir、config_path、get_config 関数は、config.json から Shotgun の設定を読み込むために用意しました。
find_unique 関数は、一意のエンティティを取得するために用意しました。
shotgun_api3.Shotgun クラスには、一つのエンティティを取得する find_one メソッドが用意されていますが、ソースコードを見てみると一意のエンティティが取得できる仕様となっていません(*3)。そのため、指定した条件に当てはまるエンティティが一意であるときだけエンティティを取得し、一意でない場合は IdentityError 例外を送出する find_unique 関数を定義しています。
*3 find_one メソッド内では、find メソッドを使ってエンティティを取得し、一つ以上のエンティティが取得された場合に、リストの先頭のエンティティのみを返す実装となっています。
find_one メソッドのソースコード(2020/1/17 時点の GitHub master ブランチ): https://github.com/shotgunsoftware/python-api/blob/e63f83a7799982850b28494df7cef813f7d232fd/shotgun_api3/shotgun.py#L835
find メソッドのリファレンス: https://developer.shotgunsoftware.com/python-api/reference.html?highlight=find#shotgun_api3.shotgun.Shotgun.find
3. ファイルアップローダーと Slack の連携
3.1. Slack Incoming Webhook の URL 取得
Slack App を登録し、完了メッセージを送信するための Incoming Webhook の URL を発行します。この URL にメッセージを送信すると、設定したチャンネルにメッセージが表示されます。
3.1.1. Slack のワークスペースとチャンネルの用意
まずは Slack のワークスペースとチャンネルを用意します。
ワークスペースを作成する場合は、Slack のサイト(https://slack.com/intl/ja-jp/)の「ワークスペース」から手順に従って新規作成します。
次に、メッセージの送信先チャンネルを作成します。今回は、「message_from_myuploader」というチャンネル名とします。
3.1.2. Slack App の作成と Incoming Webhook URL の取得
次に、Getting started with Incoming Webhook のページ(https://api.slack.com/messaging/webhooks#)の「Create your Slack app」をクリックしてアプリケーション作成ページに飛びます。
「Create an App」をクリックするとダイアログが表示されるので、「App Name」を入力し、Slack のワークスペースを選択して「Create App」をクリックします。
アプリケーションが作成されたら、アプリケーションの設定画面の「Incoming Webhooks」を選択し、「Activate Incoming Webhooks」を On にします。
「Add New Webhook to Workspace」ボタンが表示されるのでクリックします。
そうすると、権限をリクエストしてくるので、投稿先のチャンネルを選択して「許可する」をクリックします。
「Incoming Webhooks」のページに、メッセージ送信先の URL が新規に作成されます。この URL を config.json に保存します(3.2.3. スクリプトの全体像 を参照ください)。
※ Webhook URL はパスワードやトークンと同様に、公開された場所に保存しないようにしてください。
3.2. Slack チャンネルへの通知
3.2.1. requests モジュールのインストール
前回作成した仮想環境 venv_shotgun_scripting に、HTTP リクエスト(*4)を送信するための requests モジュール(*5)をインストールします。
cd .virtualenvs\venv_shotgun_scripting Scripts\activate.bat python -m pip install requests deactivate.bat
3.2.2. 通知処理の実装
上記でインストールした requests モジュールを使い、Incoming Webhook URL に HTTP リクエストを送信する部分を実装していきます。
まずは、送信に必要なデータを作成していきます。
メッセージデータを JSON として送るため、Content-type ヘッダー(*6)を application/json に設定します。
headers = {"Content-type": "application/json"}
次に、JSON オブジェクト(Python の辞書に相当)としてメッセージを作成します。メッセージには、アップロード完了メッセージと共に、アップロードに成功した数、エラーの数、合計を表示するようにします。
data = { "text": "アップロードが完了しました\n成功: {0}\nエラー: {1}\n合計: {2}".format( len(uploaded_list), len(error_list), len(target_list) ) }
これらの情報を付加し、Webhook URL にリクエストを POST(*7)します。POST が完了するとレスポンスが返ってくるので、そこに含まれているステータスコード(*8)を表示します。今回のケースでは、Slack へのメッセージ送信が成功していれば「200 OK」が返ってきます。
response = requests.post(webhool_url, json=data, headers=headers) print("Status code: {0} {1}".format(response.status_code, response.reason))
3.2.3. スクリプトの全体像
config.json の内容は下記のようになります(api_key と webhook_url はダミーです)。Shotgun の設定と Slack の設定を JSON のオブジェクトとして持っています。
{ "shotgun": { "target_dir": "images", "url": "https://smiletechnologyunited.shotgunstudio.com", "script_name": "myuploader", "api_key": "fouetfj8uebo3bjflneghqorp", "project_id": 90 }, "slack": { "webhook_url": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" } }
cli.py の全体像を以下に示します。改良したアップローダーの処理の後ろに、Slack への送信処理を追加しています。
import json import os import sys import requests from shotgun_api3 import Shotgun class IdentityError(Exception): pass def base_dir(): """このファイルが置かれているフォルダのパスを返す :rtype: str """ return os.path.dirname(os.path.abspath(__file__)) def config_path(): """config.jsonファイルのパスを返す :rtype: str """ return os.path.join(base_dir(), "config", "config.json") def get_config(): """config.jsonの中身を取得して返す :rtype: dict """ with open(config_path(), "r") as fp: config = json.load(fp) return config def find_unique(shotgun, entity_type, filters, fields): """一意のエンティティを取得する 対象エンティティが複数存在したらIdentityErrorを送出する :type shotgun: shotgun_api3.Shotgun :type entity_type: str :type filters: list :type fields: list :rtype: dict """ _entities = shotgun.find(entity_type, filters, fields) if not _entities: return None elif len(_entities) == 1: entity = _entities[0] return entity else: raise IdentityError def main(): """メイン関数 :rtype: int """ config = get_config() # Shotgunの設定 shotgun_config = config["shotgun"] target_dir = shotgun_config["target_dir"] url = shotgun_config["url"] script_name = shotgun_config["script_name"] api_key = shotgun_config["api_key"] project_id = shotgun_config["project_id"] project = {"type": "Project", "id": project_id} # Shotgunインスタンスの取得 shotgun = Shotgun(url, script_name=script_name, api_key=api_key) # 対象のエンティティをバージョン、アップロード先のフィールドをsg_uploaded_movieとします entity_type = "Version" field_code = "sg_uploaded_movie" error_list = [] target_list = os.listdir(target_dir) for _file in target_list: try: filters = [["project", "is", project], ["code", "is", _file]] fields = ["id"] version = find_unique(shotgun, entity_type, filters, fields) except IdentityError: error_list.append(_file) continue if not version: # ファイル名と同じ名前のバージョン名と、所属するプロジェクトの情報を持たせます entity_data = {"code": _file, "project": project} # バージョンエンティティを作成します version = shotgun.create(entity_type, entity_data) # versionにはどのようなデータが入っているでしょうか? print(version) # 作成したバージョンのidを取得します version_id = version["id"] file_path = os.path.join(target_dir, _file) # 作成したバージョンにファイルをアップロードします shotgun.upload( entity_type, version_id, file_path, field_name=field_code, display_name=_file ) # Assetの名前は、ファイル名から拡張子を除いたものにします name, ext = os.path.splitext(_file) asset = find_unique( shotgun, "Asset", [["project", "is", project], ["code", "is", name]], ["id"] ) if not asset: asset_entity_data = {"code": name, "project": project} asset = shotgun.create("Asset", asset_entity_data) # 作成されたAssetの情報を確認します print(asset) # 新規作成時にはリンクを作成できないので、アップデートで作成します asset_id = asset["id"] link_data = {"sg_versions": [{"type": "Version", "id": version_id}]} shotgun.update("Asset", asset_id, link_data) print("") uploaded_list = sorted(list(set(target_list).difference(set(error_list)))) if uploaded_list: print("アップロードされたファイル: {0}".format(len(uploaded_list))) for _file_name in uploaded_list: print(_file_name) print("") if error_list: print("重複しているファイル: {0}".format(len(error_list))) for _error_message in error_list: print(_error_message) print("") # Slackの設定 slack_config = config["slack"] webhook_url = slack_config["webhook_url"] # コンテンツのデータ形式を指定します headers = {"Content-type": "application/json"} # 送信するメッセージ data = { "text": "アップロードが完了しました\n成功: {0}\nエラー: {1}\n合計: {2}".format( len(uploaded_list), len(error_list), len(target_list) ) } # POSTメソッドでリクエストを送信し、レスポンスを取得します response = requests.post(webhook_url, json=data, headers=headers) print("Status code: {0} {1}".format(response.status_code, response.reason)) return 0 if __name__ == "__main__": sys.exit(main())
4. 実行
前回同様、myuploader.bat をダブルクリックで実行します。
それではやっていきましょう。
バージョンとアセットのセットが作成されていきます。
アップロードが完了すると、コンソールに結果の詳細が表示されます。
ステータスコードが「200 OK」で返ってきているので、Slack チャンネルへのメッセージ送信も正常に行えています。Slack を確認してみましょう。
Slack の「message_from_myuploader」チャンネルへアップロード完了のメッセージが届いています。
Shotgun プロジェクトにアセットが作成され、バージョンへのリンクがされていることが確認できます(sg_versions フィールド)。
バージョンからも、アセットへのリンクが作成されていることが確認できます(entity フィールド)。
5. 今回のまとめ
● 基本的な処理が作成できたら、ツールの使い勝手を良くするために、より実用的な処理を追加していきます。
● トークンや Webhook URL などの重要なデータは、スクリプトから分離しておくと管理しやすくなります。
● Shotgun へのアクセスは基本、shotgun_api3.Shotgun クラスのメソッドを使用し、欲しい機能がなければ実装します。
● Slack App の Incoming Webhook と requests モジュールを組み合わせて、Slack チャンネルへメッセージを送信することができます。
6. 付録
6.1. Shotgun 関連
● Python API
○ API Reference: https://developer.shotgunsoftware.com/python-api/reference.html
○ GitHub リポジトリ: https://github.com/shotgunsoftware/python-api
6.2. HTTP 関連
● MDN web docs
○ HTTP: https://developer.mozilla.org/ja/docs/Web/HTTP
6.3. Slack 関連
● Slack: https://slack.com/intl/ja-jp/
● Sending messages using Incoming Webhooks: https://api.slack.com/messaging/webhooks#