FlaskでAPIを作って、受け取ったデータをGoogleスプレッドシートに書き出す

# つくるもの

Pythonで取得したデータを順次GASに返却し、GAS側で受け取ったデータをスプレッドシートに書き込む。
GASには時間制限があるため、開始位置と終了位置を指定し、細かく区切って返却するようにする。

- Flask側アプリ
    - Flaskで作成
    - Herokuで公開
    - 処理内容：開始位置と終了位置を受け取りデータ（今回は仮データ）を返す
- GAS側
    - 実行時、開始位置と終了位置をFlaskアプリに投げ、受け取ったJSONデータをGoogleスプレッドシートに書き込む
    - 一回の実行で10ずつデータを受け取り、50受け取るまで繰り返し実行する

# 環境
- MAC
- Python 3.11.3
- Flask 3.0.2

# Flask側アプリの作成

### flaskのinstall

```
pip install Flask
```

### 開発フォルダの作成

```
mkdir my-test-api
cd my-test-api
```

### APIの作成

pythonファイルのファイル名は任意

```python:main.py
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

@app.route('/process', methods=['POST'])
def process_data():
    # POSTリクエストからstart_numberとend_numberの値を取得
    start_number = int(request.form['start_number'])
    end_number = int(request.form['end_number'])
    
    results = []
    # start_numberとend_numberの範囲で処理を繰り返す
    for i in range(start_number, end_number + 1):
        # ここでstart_numberとend_numberを使って何らかの処理を行う
        result = f"実行結果{i}"
        # 結果をリストに追加
        results.append([i, result])
    
    # 処理結果をJSON形式で返す
    return jsonify(results)

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
```

### ローカル環境でのテスト

先ほど作成したFlaskアプリケーションを実行する

```
Python main.py
```

### テスト用アプリの作成

先ほど作ったFlaskアプリケーションをテストするためのFlaskアプリケーションを作成する。

```python:test.py
import requests,json

url = 'http://127.0.0.1:5000/process'
data = {'start_number': 10, 'end_number': 20}
response = requests.post(url, data=data)

# エスケープされたJSON文字列をデコードしてリストに変換
decoded_list = json.loads(response.text)

print(decoded_list)
```

### テスト用アプリケーションの実行

先ほどのFlaskアプリケーションを起動したまま、別ターミナルで実行する

```
Python test.py
```

### 実行結果

```
$ python test.py
[[10, '実行結果10'], [11, '実行結果11'], [12, '実行結果12'], [13, '実行結果13'], [14, '実行結果14'], [15, '実行結果15'], [16, '実行結果16'], [17, '実行結果17'], [18, '実行結果18'], [19, '実行結果19'], [20, '実行結果20']]
```

# FlaskアプリケーションをHerokuにデプロイする

### runtime.text

[herokuでサポートしているPythonのバージョン](https://devcenter.heroku.com/ja/articles/python-support)を確認する。

```
echo "python-3.11.3" > runtime.txt
```

### Procfile

```
install gunicorn
echo web: gunicorn main:app --log-file=- > Procfile
```

### requirements.txt

```
pip freeze > requirements.txt
```

**gunicorn**の行が出力されていることを確認。

### ファイルの作成完了

```
my_test_app/
│
├── main.py
├── Procfile
├── requirements.txt
└── runtime.txt
```

### heroku CLI インストール

```
brew tap heroku/brew && brew install heroku
```

### heroku ログイン

```
heroku login
```

### アプリ作成

アプリ名は他と被らないような任意のサイト名を設定する。

```
heroku create アプリ名
```

```
（例）これだと被るので作成できない
heroku create my-test-api
```

### リモートブランチ作成

my-test-apiの部分は作成したアプリ名に置き換え

```
heroku git:remote -a my-test-api
```

### Herokuにmainブランチをpushする

```
git push heroku main
```

## デプロイ後の動作確認

先ほど作成した`test.py`を流用する

```python:test.py
import requests,json

url = '[★この部分をデプロイ後のURLに差し替え]/process'
data = {'start_number': 10, 'end_number': 20}
response = requests.post(url, data=data)

# エスケープされたJSON文字列をデコードしてリストに変換
decoded_list = json.loads(response.text)

print(decoded_list)
```

### 実行

```
Python test.py
```

### 実行結果

```
$ python test.py
[[10, '実行結果10'], [11, '実行結果11'], [12, '実行結果12'], [13, '実行結果13'], [14, '実行結果14'], [15, '実行結果15'], [16, '実行結果16'], [17, '実行結果17'], [18, '実行結果18'], [19, '実行結果19'], [20, '実行結果20']]
```


# GAS側アプリの作成

### 前準備：スプレッドシートの作成

作成したGoogleスプレッドシートのURLからIDを取得する

```
https://docs.google.com/spreadsheets/d/[この部分がID]/edit#gid=0
```

シート名を`結果出力`に変更

### GASアプリケーションの作成

[GAS](https://script.google.com/)　から「新しいプロジェクト」を作成する。

```javascript:
var url = "[デプロイしたURL]/process";

function myFunction() {
  // スプレッドシートのシート名を指定します
  var sheetName = "結果出力";
  // スプレッドシートを取得します
  var ss = SpreadsheetApp.openById("[スプレッドシートのID]");
  var sheet = ss.getSheetByName(sheetName);

  // スクリプトプロパティから最後に処理した値を取得
  var startNumberStr =
    PropertiesService.getScriptProperties().getProperty("start_number");
  var endNumberStr =
    PropertiesService.getScriptProperties().getProperty("end_number");

  // 値がない場合は初期値を設定
  if (!startNumberStr || !endNumberStr) {
    // スプレッドシートのデータをすべてクリアします
    sheet.clear();

    startNumber = 1;
    endNumber = 10;
  } else {
    startNumber = parseInt(startNumberStr);
    endNumber = parseInt(endNumberStr);
  }

  var payload = {
    start_number: startNumber.toString(),
    end_number: endNumber.toString(),
  };

  var options = {
    method: "post",
    payload: payload,
  };

  var response = UrlFetchApp.fetch(url, options);

  // 取得したデータを処理する（ここではログに出力する）
  Logger.log(response); // 取得したデータをログに出力
  // データをシートに書き込む
  let json = JSON.parse(response)
  var row = startNumber;
  json.forEach(function(rowData) {
    rowData.forEach(function(cellData, col) {
      sheet.getRange(row, col + 1).setValue(cellData);
    });
    row++;
  });

  // 次の範囲をセットアップ
  startNumber += 10;
  endNumber += 10;

  // スクリプトプロパティに次回処理する値を保存
  PropertiesService.getScriptProperties().setProperty(
    "start_number",
    startNumber
  );
  PropertiesService.getScriptProperties().setProperty("end_number", endNumber);

  // 100まで取得が終わったらトリガーを削除して処理を終了する
  if (endNumber > 50) {
    ScriptApp.getProjectTriggers().forEach(function (trigger) {
      ScriptApp.deleteTrigger(trigger);
    });
    // ScriptProperties を削除します
    PropertiesService.getScriptProperties().deleteAllProperties();
  } else {
    // 5秒後に自身を再実行するトリガーを設定
    ScriptApp.newTrigger("myFunction").timeBased().after(5000).create();
  }
}
```

### GASアプリケーションの実行

![スクリーンショット 2024-03-31 22.49.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692485/cae8f690-5400-02d9-5516-d13dbe488061.png)


![スクリーンショット 2024-03-31 22.45.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692485/bdc2d08e-0990-1b50-b3b6-f00e95b22c0a.png)

### トリガーとスクリプトプロパティがセットされていることを確認

#### トリガー

![スクリーンショット 2024-03-31 22.50.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692485/4e4d1178-ccfc-a995-50ed-9ed710f6cf27.png)

#### スクリプトプロパティ

![スクリーンショット 2024-03-31 22.50.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692485/b458f2cd-b2f1-7e68-5bb0-626ea7217440.png)

### 最終的な実行結果

待っていれば、順次実行されるはず。
5回すべて実行完了するまでに7分程度かかっているようです。

![スクリーンショット 2024-03-31 23.05.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692485/f2349251-01d7-6330-cb59-1e38cd06c57f.png)

![スクリーンショット 2024-03-31 23.06.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692485/4fcd8ba8-5ec1-db5f-f969-19e77656ef03.png)

# 終わりに

Flaskアプリケーション側の処理を変えれば、さまざまな応用ができると思います。