Google Apps ScriptのExecution REST APIで他のアプリと接続方法

#Google Apps Scriptとは何？
G suiteの中にあるDoc、Spreadsheet、Formの内部処理を自動化するために使用するものです。MicrosoftのOffice suiteの中にあるExcelやWordの内部処理を自動化するために組むvba scriptと同じ役割をもっています。

##script editorを開く
1. Standalone
![google app script standalone.png](https://qiita-image-store.s3.amazonaws.com/0/221025/18246225-026f-260d-f063-dcf9a0a56b77.png)


2. DocやSpreadsheetの中から
![not_standalone.png](https://qiita-image-store.s3.amazonaws.com/0/221025/54b8d9f4-93cc-cc62-ed21-a677c175f1ff.png)

##Execution REST APIについて
Google Apps ScriptのRESTインタフェース経由で別のアプリがscriptの関数を呼び出すことができます。具体的な例としては別のアプリから下記の操作が実行できます。

- Google DocやGoogle Spreadsheetのデータの取得
- Google Doc、Google Spreadsheet、Google Driveのファイルの基本操作(生成、更新、削除など)

##REST I/Fを使ってapps script+pythonでプログラムを組んでみましょう
###プログラムの目的
Google Drive上フォルダサイズが表示されないことはみんな知っているかと思います。フォルダの管理のためにプログラムを組んで自分のGoogle Driveにあるフォルダのstorageサイズを降順で出力させましょう

##流れ
下記URLを参照してください。この記事で説明が不足している点をクリアにしていきたいと思います。
https://developers.google.com/apps-script/guides/rest/quickstart/python

###環境
- python2.6(pip)以上やpython3(pip3)
- 有効なGoogleアカウントとGoogle drive
- アプリと連携のためにtarget Apps Script

###target apps scriptをeditorから作成（ファイル名は任意）
```javascript


/**
 * The function in this script will be called by the Apps Script Execution API.
 */
/**
 * Return the set of folder names contained in the user's root folder as an
 * object (folder set along with folder size).
 * @return {Object} A set of folder names,size keyed by folder ID.
 */
function getFoldersUnderRoot() {
  var root = DriveApp.getRootFolder();
  var folders = root.getFolders();
  var folderSet = {};
  while (folders.hasNext()) {
    var folder = folders.next();
    // send list
    folderSet[folder.getId()] = [folder.getName(), getTotalSizeOfCurrentFolder(folder)];
  }
  return folderSet;
}

function getTotalSizeOfCurrentFolder(folder) {
  var files = folder.getFiles();
  var totalSize = 0;
  while (files.hasNext()) {
    var file = files.next();
    totalSize = totalSize + file.getSize();    
  }
  Logger.log(totalSize);
  return totalSize;
}
```
###target apps scriptの配置
「公開」メニューから「実行可能APIとして導入」を選ぶ
![Deploy as API executable .png](https://qiita-image-store.s3.amazonaws.com/0/221025/2fa57066-7eb9-7025-b0b3-e60e366951fe.png)

下記のようにtarget設定を行う、そのあと配置ボタンを押す
![deploy.png](https://qiita-image-store.s3.amazonaws.com/0/221025/9eb19cae-5e00-ba45-ca4e-9143efbec9ae.png)

表示されるAPI IDをメモーしておいて画面を閉じる
![API ID.png](https://qiita-image-store.s3.amazonaws.com/0/221025/e723b61a-920b-4dc6-4c2c-473d2c8e8b8e.png)


##Step1
###target apps scriptの設定
1.「リソース」メニューから「Cloud Platformプロジェクト」を選ぶ
![Cloud Platformproject.png](https://qiita-image-store.s3.amazonaws.com/0/221025/e78c5425-d281-78f4-7ad0-f219ce396633.png)


2. ダイアログの青いリンクのProject IDを押す
![Project ID.png](https://qiita-image-store.s3.amazonaws.com/0/221025/8dbe66f8-d6d2-a60d-3a36-9e1e1221d55d.png)


3. APIs & servicesからLibraryを選ぶ
![Choose Library.png](https://qiita-image-store.s3.amazonaws.com/0/221025/bfa1c620-0b66-c052-405a-39f3746dde28.png)


4. 検索のGoogle APIsタブから"Google Apps Script API"を探してクリックする
![Search.png](https://qiita-image-store.s3.amazonaws.com/0/221025/7db101a7-6d65-1001-bb4e-b6de368d57a6.png)
![Google Apps Script API.png](https://qiita-image-store.s3.amazonaws.com/0/221025/074b35fb-e39a-1f83-85ce-b686b9898785.png)

5. Enable APIを押す(一回有効になったら次からManage APIが表示される
![Google Apps Script API Manage_Enable.png](https://qiita-image-store.s3.amazonaws.com/0/221025/3cdee682-eaa5-4339-42f9-7258ae865d82.png)

6.　左側にあるCredentialsタブを選択する
Create Credentialsの中からOAuth client IDを選択する
**認証IDとして使える**
![Create Credentials.png](https://qiita-image-store.s3.amazonaws.com/0/221025/c2a10d52-cf85-40a3-20fa-037a61544bc4.png)

7.　Application typeでOtherを選んでCreateボタンを押す
![Other.png](https://qiita-image-store.s3.amazonaws.com/0/221025/2a54e1bb-9594-fe63-c3f2-a35695e73f03.png)

8.　次表示されるダイアログをOKで閉じる
9.　ID情報のJSONをダウンロードする(Download JSON)
10. このファイルをそのままPythonのアプリのパスに保存しておく
**(記事にclient_secret.jsonとして保存と書いてあるが、認証情報が他のtarget apps scriptとかぶらないため名前の変更をしないまま使う)**

##Step2
###Pythonのアプリ側の準備
- 下記のlibraryのpip install
    - sudo pip/pip3 install httplib2
    - sudo pip/pip3 install --upgrade google-api-python-client  
    (上記のインストール失敗の場合)
    - **sudo pip/pip3 install --upgrade google-api-python-client --ignore-installed six**

##Step3
###Pythonでscriptを呼び出すアプリを作成（ファイル名は任意）
```python3

from __future__ import print_function
import httplib2
import os
import fnmatch

from apiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage

from apiclient import errors

try:
    import argparse
    flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
except ImportError:
    flags = None

# If modifying these scopes, delete your previously saved credentials
# at ~/.credentials/script-python-quickstart.json
SCOPES = 'https://www.googleapis.com/auth/drive'
for file in os.listdir('.'):
 if fnmatch.fnmatch(file,'client*'):
  CLIENT_SECRET_FILE = file
APPLICATION_NAME = 'Google Apps Script Execution API Python Quickstart'


def get_credentials():
    """Gets valid user credentials from storage.

    If nothing has been stored, or if the stored credentials are invalid,
    the OAuth2 flow is completed to obtain the new credentials.

    Returns:
        Credentials, the obtained credential.
    """
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_json_file = os
    credential_path = os.path.join(credential_dir, CLIENT_SECRET_FILE)

    store = Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        if flags:
            credentials = tools.run_flow(flow, store, flags)
        else: # Needed only for compatibility with Python 2.6
            credentials = tools.run(flow, store)
        print('Storing credentials to ' + credential_path)
    return credentials

def main():
    """Shows basic usage of the Apps Script Execution API.

    Creates a Apps Script Execution API service object and uses it to call an
    Apps Script function to print out a list of folders in the user's root
    directory.
    """
    SCRIPT_ID = 'ENTER_YOUR_SCRIPT_ID_HERE'

    # Authorize and create a service object.
    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    service = discovery.build('script', 'v1', http=http)

    # Create an execution request object.
    request = {"function": "getFoldersUnderRoot"}

    try:
        # Make the API request.
        response = service.scripts().run(body=request,
                scriptId=SCRIPT_ID).execute()

        if 'error' in response:
            # The API executed, but the script returned an error.

            # Extract the first (and only) set of error details. The values of
            # this object are the script's 'errorMessage' and 'errorType', and
            # an list of stack trace elements.
            error = response['error']['details'][0]
            print("Script error message: {0}".format(error['errorMessage']))

            if 'scriptStackTraceElements' in error:
                # There may not be a stacktrace if the script didn't start
                # executing.
                print("Script error stacktrace:")
                for trace in error['scriptStackTraceElements']:
                    print("\t{0}: {1}".format(trace['function'],
                        trace['lineNumber']))
        else:
            # The structure of the result will depend upon what the Apps Script
            # function returns. Here, the function returns an Apps Script Object
            # with String keys and values, and so the result is treated as a
            # Python dictionary (folderSet).
            folderSet = response['response'].get('result', {})
            if not folderSet:
                print('No folders returned!')
            else:
                folderSet = SortFolderIDAccToSize(folderSet)
                print('Folders under your root folder:')
                for (folderId, folder) in folderSet:
                    print("\t{0} {1}".format(folder[0], GetHumanReadable(folder[1])))
    except errors.HttpError as e:
        # The API encountered a problem before the script started executing.
        print(e.content)

# return sorted folderSet as list
def SortFolderIDAccToSize(folderSet):
    return sorted(folderSet.items(), key=lambda x: x[1][1], reverse=True)

# Size format
def GetHumanReadable(size,precision=2):
    suffixes=['B','KB','MB','GB','TB']
    suffixIndex = 0
    while size > 1024 and suffixIndex < 4:
        suffixIndex += 1 #increment the index of the suffix
        size = size/1024.0 #apply the division
    return "%.*f%s"%(precision,size,suffixes[suffixIndex])

if __name__ == '__main__':
    main()
```
**上記の***SCRIPT_ID = 'ENTER_YOUR_SCRIPT_ID_HERE'***にメモーしていたAPI IDを入力してソース修正を行う**

##Step4
###プログラムを実行する
- $ python3 quickstart.py
- 実行されたあとに開いているブラウザーから認証の要求が行われる
- 許可をあげたら自動的にアプリ側のコンソールに出力が表示される
- 一回許可あげたあとに認証情報が~/.credentialsのフォルダに保存され、次から聞かれない
- **出力結果例**  
    Folders under your root folder:  
    *  Import 1.69MB  
    *  Spark 538.07KB  
    *  社内勉強会リソース 190.42KB  
    *  open edX 5.85KB  
    *  backup 0.00B   
(自分のGoogle Driveにあるフォルダが表示されています):relaxed:

##注意点
- Python 2.7を使っている場合下記を注意しましょう
    - pipで必要なlibraryをインストールする
    - $ python quickstart.pyでアプリ実行
    - メソッド
        - iterItems()が2.7用、items()が3.0用
- 本scriptがフォルダの中にあるフォルダ(**nested folder**)のサイズを計算してくれない
    - フォルダの中にファイルがある前提でscriptが正しく動く
    - nested folderの場合、scriptの改善が少しい必要
- script側の変更修正が必要な場合、新しいバージョンで再配置を行う
![re-deploy.png](https://qiita-image-store.s3.amazonaws.com/0/221025/877b4ddd-4c2b-edb0-a235-bc4a3c7d6b8d.png)
更新の効果が出なかった場合、~/.credentialsの中に該当するOAuthのjsonファイルを削除する

#参考文献
- target apps script  
https://developers.google.com/apps-script/guides/rest/quickstart/target-script
- アプリの実装  
https://developers.google.com/apps-script/guides/rest/quickstart/python