flaskでAPI開発(的な）事をしてみた

#はじめに
今回行う事は、
- flaskを使ったAPI開発
- 作ったAPIをWEBアプリ（Flask)で利用する

きっかけとしては、HTTPメソッドなどのWeb上で行われている通信を理解する事に適しているのではないかと思ったから。

#作る成果物
**スケジュールツール**

- 一覧画面 => 月別のスケジュールを見る事ができる
- API
    - 入力画面 => リマインダーを入力
    - 削除・編集 => リマインダーを削除・編集を行う


##前提環境・ディレクトリ
Flask 0.12.2
Python 3.6.7

.(root)
├ static
│    └ index.css
├ templates
│       ├── create.html => 追加フォーム
│       └── index.html => スケジュール画面
├ view.py  => APIを利用する
└ view_api.py => APIを作成する

```python:view.py
from flask import Flask, render_template, redirect, request, url_for
import calendar
from calendar import Calendar
import os
import datetime
import requests
import json

app = Flask(__name__)

@app.context_processor
def override_url_for():
    return dict(url_for=dated_url_for)

def dated_url_for(endpoint, **values):
    if endpoint == 'static':
        filename = values.get('filename', None)
        if filename:
            file_path = os.path.join(app.root_path,
                                     endpoint, filename)
            values['q'] = int(os.stat(file_path).st_mtime)
    return url_for(endpoint, **values)


if __name__ in "__main__":
    app.run(debug=True)
```
「override_url_for」,「dated_url_for」　それぞれのメソッドに関しましては、下記のURLから参照しました（中身がどのように行われているのかは、理解ができていません・・・）
>https://aroundthedistance.hatenadiary.jp/entry/2015/01/28/101902
>https://qiita.com/ymko/items/b03499aefcceec10ef8f


```python:view_api.py
from flask import Flask, jsonify, render_template, redirect, request, url_for
import calendar
from calendar import Calendar
import os
import datetime
import requests

app = Flask(__name__)

#簡易DB
array_remind = [
    # {'title':"hoge", "content": "hoge", "day": "2019-08-29"},
    # {'title':"hoge", "content": "hoge", "day": "2019-08-04"},
]

if __name__ in "__main__":
    app.run(debug=True, threaded=True, port=3000)
```
今回は、pythonに入っている「sqlite3」などは使わず、配列と辞書で作っています。

##進め方
機能別に進めていきたいと思います。
例)一覧画面の表示の仕方
view.py => view_api.py => index.html　と表示をしていきます。


##API機能
最初に、APIの全体像から見ていき、機能別にクローズアップをしていきたいと思います。

```python:view_api.py

#一覧画面を取得
@app.route("/api/index")
def schedule_index():
    return jsonify(array_remind)

#新規追加
@app.route("/api/create", methods=["POST"])
def create():
    if request.method == "POST":
        data = {
            "title": request.args.get("title",""),
            "content": request.args.get("context",""),
            "day": request.args.get("day","")
        }
        data_s = db_remind(data)
        return '', 204

#詳細
@app.route("/api/detail", methods=["GET"])
def detail():
    return jsonify(request.form)


def db_remind(dict_params):
    array_remind.append(dict_params)
    return array_remind
```


##一覧画面(view_api.py => view.py => index.html) 

```python:view_api.py
@app.route("/api/index")
def schedule_index():
    return jsonify(array_remind) #array_remindは簡易DBの事です
```

view.pyから送られたURLにマッチしている@app.routeのメソッドが呼び出される。
`jsonify()`は、JSONデータに変換してくれます。

これだけでも、curlコマンドを使うことで、データがJSON型で表示させる事ができます。

```
curl {URL}/api/index
```

これを叩けば・・・・

```
[
  {
    "content": "hoge", 
    "day": "2018-08-29", 
    "title": "hoge"
  }, 
  {
    "content": "hoge", 
    "day": "2018-08-04", 
    "title": "hoge"
  }
]
```

と、表示され、無事に取得できる（はず・・・)。

それでは、これをWebアプリに表示できるようにしたいと思います。

```python:view.py
def index():
    url_info = "{URL}/api/index"
    month_current = datetime.date(year=2019,month=8, day=1)
    month_days = Calendar().monthdatescalendar(2019, 8)
    context_json = requests.get(url_info).text
    context = json.loads(context_json)[0]["title"]
    get_date = requests.get(url_info).text
    designated_date = json.loads(get_date)[0]["day"]
    params = {
        'context':  context,
        'day': datetime.datetime.strptime(designated_date,"%Y-%m-%d").day #=> str型をdatetime型（日付型）に変えています。
    }
    week_name = ['月','火','水','木','金','土','日']
    return render_template(
        "index.html",month_day = month_days, week_name = week_name, month_current = month_current.month, params = params
    )
```

```python:view.py
context_json = requests.get(url_info).text
get_date = requests.get(url_info).text
```
この２つが、view_api.pyに対して、リクエストを送っています。
`.text`は、テキスト型に直し、変換しやすくしてくれます。

```python:view.py
context = (context_json)[0]["title"]
designated_date = json.loads(get_date)[0]["day"]
```

`json.loads`によって、JSON型から、配列と辞書を組み合わせ形にします。


```python:view.py
return render_template(
        "index.html",month_day = month_days, week_name = week_name, month_current = month_current.month, params = params
)
```

`render_template`を使って、表示したいHTMLとパラメーターを指定する。

```index.html
<!DOCTYPE html>
<html>
  <head>
      <link rel=stylesheet type=text/css href="{{ url_for('static', filename='index.css')}}">
      <meta charset="utf-8">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  </head>
  <body>
    <h1>{{month_current}}月</h1>
    <table class="table">
        <thead>
            <tr>
                {% for week in week_name %}
                    <th>{{week}}</th>
                {% endfor %}
            </tr>
        </thead>
        {% for index in month_day %}
            <tr>
                {% for w in index %}
                    <td>
                        <a href="./{{w.day}}">{{w.day}}</a>
                        {% if params['day'] == w.day %}
                            <div class="box">{{params['context']}}</div>
                        {% endif %}
                        
                    </td>
                {% endfor %}
            </tr>
        {% endfor %}
    </table>
    
  </body>
</html>
```

カレンダーになるように、for文を回していき完成！


##新規追加（view.py => create.html =>  view.py => view_api.py

```python:view.py
@app.route("/<int:post_id>")
def add_form(post_id):
    return render_template("create.html", des_id=post_id)
```

カレンダーの数字をクリックすると、「`create.html`に表示される」ようにしている。


```create.html
<body>
    <form method="POST" action="/post">
        <div class="form-group">
          <label>title</label>
          <input type="text" name="title" class="form-control">
          <small id="emailHelp" class="form-text text-muted">内容を記入してね！</small>
        </div>
        <div class="form-group">
          <label>context</label>
          <input type="text" name="context" class="form-control">
        </div>
        <div class="form-group">
            {% if des_id <= 9 %}
              <input type="date" name="day" class="form-control" value="2018-08-0{{des_id}}">
            {% else %}
              <input type="date" name="day" class="form-control" value="2018-08-{{des_id}}">
            {% endif %}
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</body>
```


`<body>`の部分だけ表示しました。`<form>`に`method=POST`と指定する。

```python:view.py
@app.route("/post", methods=["POST"])
def create():
    url = "{URL}/api/create"
    params = {
        "title": request.form['title'],
        "context": request.form['context'],
        "day": request.form['day']
    }
    requests.post(url, params=params)
    return redirect(url_for("index"))

```

一覧画面の時と、ほとんど変わらない。しかし・・・・
`requests.post`を使っている。これは、POSTリクエストを送るために使用している。
`requests.get`とほとんど同じ処理を行うため、引数なども同じである。


```python:view_api.py
@app.route("/api/create", methods=["POST"])
def create():
    if request.method == "POST":
        data = {
            "title": request.args.get("title",""),
            "content": request.args.get("context",""),
            "day": request.args.get("day","")
        }
        db_remind(data)
        return '', 204

```

`request.args.get` は、URLに書かれている「127.0.0.1:?title="hoge"....」の「変数」に含まれる値（hoge)を取得できるメソッドである。
`db_remind`メソッドに辞書型のdataを引数にして渡し、DBに保存される。

curlコマンドを使うのであれば、下記のようになるが、`return`が「204を返すだけ」となっているため、何も表示はされない。

```
curl -X POST -H "Content-Type: application/json" -d '{"content": "hoge"}' {URL}/api/create
```

しかし、起動したサーバーを確認すると、

```
....  "POST /api/create HTTP/1.1" 204 - 
```

というログが表示されている（はず・・・・）。もし、表示されていれば、成功である。
もし、エラーや違う数字がでた場合は、「HTTPステータスコード　数字」と検索をかければ、原因がわかる（かも）


##編集(view.py => edit.html => view.py => view_api.py)

```python:view.py
def add_form(post_id):
    url_info = "{URL}/api/index"
    json_data = requests.get(url_info).text
    if json.loads(json_data): #=> 中身は[]になっています。
        get_id = json.loads(json_data)[0]['day']
        day = datetime.datetime.strptime(get_id,"%Y-%m-%d").day
        if day == post_id:
            params = {
                "title": json.loads(json_data)[0]['title'],
                "context": json.loads(json_data)[0]['context']
            }
            return render_template("edit.html", des_id=post_id, params=params)
        else:
            return render_template("create.html", des_id=post_id)
    else:
        return render_template("create.html", des_id=post_id)
```

カレンダーの数字をクリック => すでに、APIのDBに1つでも保存されているときは、True方向に進む => DBに保存されている日付と一致していれば、さらにTrue方向に進み => edit.htmlへ（終了）
=> ない場合は新規作成(create.htmlへ)（終了）

```edit.html
<form method="POST" action="/post">
        <input type="hidden" value="put" name="_method">
        <div class="form-group">
          <label>title</label>
          <input type="text" name="title" class="form-control" value="{{params['title']}}">
          <small id="emailHelp" class="form-text text-muted">内容を記入してね！</small>
        </div>
          .
          .
          .
</form>
```

ほとんど、cerate.htmlと変わらない仕様になっている。
しかし、`<input type="hidden" value="put" name="_method">`は、~~Flaskの使用上？の問題で~~(HTMLのForm文がDELETEやPUTをサポートしていないらしい・・・)、PUTやDELETEができないらしいため、`hidden`を使って、送信するようにしている。

```python:view.py
def create():
    if request.form.get('_method') == "POST":
        url = "{URL}/api/create"
        params = {
            "title": request.form['title'],
            "context": request.form['context'],
            "day": request.form['day']
        }
        requests.post(url, params=params)
        return redirect(url_for("index"))
    elif request.form.get('_method') == "put":
        edit_url = "{URL}/api/edit"
        params = {
            "title": request.form['title'],
            "context": request.form['context'],
            "day": request.form['day']
        }
        requests.put(edit_url, params=params)
        return redirect(url_for("index"))

```

`request.form.get`を使って、`hidden`で隠した`value`の値を取得しています。
分岐点として、`POST`と`PUT`で分けています。`POST`の場合は、`requests.post`
`PUT`の場合は、`requests.put`で行います。

※特に引数に違いはありません。APIにリクエストを飛ばすURLを変えておくくらいです。

```python:view_api.py
@app.route("/api/edit", methods=["PUT"])
def edit():
    if request.method == "PUT":
        data = {
            "title": request.args.get("title",""),
            "context": request.args.get("context",""),
            "day": request.args.get("day","")
        }
        db_remind(data)
        return '', 204

def db_remind(dict_params):
    if request.method == "POST":
        array_remind.append(dict_params)
        return array_remind
    elif request.method == "PUT":
        #同じdayのtitleとcontentを変える
        for params in array_remind:
            if dict_params['day'] == params['day']:
                params['title'] = dict_params['title']
                params['context'] = dict_params['context']
                return array_remind
```

`PUT`の場合でも、`POST`の時と変わりはありません。
しかし、DBに保存するときの処理は変更しています。主に、変更する・される日付が一緒の場合、「title」と「context」を変える事のようにしています。


`curl`コマンドを使うと、

```
curl -X PUT -H "Content-Type: application/json" -d '{"content": "hoge"}' {URL}/api/edit
```

`POST`を`PUT`に変更をするだけです。ログも`POST`の時と変わりず、`204`が返ってくる（はず）。

##削除(index.html => view.py => view_api.py)

今回は、全て削除しています。

```index.html
<form method="POST" action="/delete">
   <input type="hidden" name="_method" value="DELETE">
   <input type="submit" class="btn btn-primary" value="全部消す">
</form>
```
編集の時と変わらずです。


```python:view.py
#PUTやDELETEはできないらしいため、methods=["POST"]にしています（HTMLのFormからは、読み取れないらしい）
@app.route("/delete", methods=["POST"])
def all_delete():
    if request.form.get("_method") == "DELETE":
        delete_url = "{URL}/api/delete"
        requests.delete(delete_url)
        return redirect(url_for("index"))
```

受け取るメソッドは「DELETE」にしたいが、Form文ではサポートされていないため、POSTの中の`_method`に埋め込んでいます。
そして、`request.form.get`で`DELETE`になっているのかを見ています。

その後は、`requests.delete(delete_url)`で、一括削除をするAPIに送ります。

```python:view_api.py
def delete():
    if request.method == "DELETE":
        array_remind.clear()
        return '', 204
```

array_remindは`list`型
そのため、`clear`メソッドを使い、空のリストにする。

curlコマンドでは、

```
curl -X DELETE -H "Content-Type: application/json"  {URL}/api/delete
```

で行う事ができる。
その後、下記を叩くと・・・・

```
curl -X GET -H "Content-Type: application/json"  {URL}/api/index
```

```
[]
```

が、返ってくる（はず・・・）


##最後に

今回、FlaskでWebアプリを作り、APIもFlaskで行ってしまったため、APIとview.pyでの処理の仕方に戸惑っていましたが、なんとかできました。
しかし、APIに関しましては、ネットの知識がほとんどになっているため、「ここが間違っている。」「これは、APIではない」と思われる方は、コメント欄などで指摘してもられると嬉しいです。