はじめに
「PythonではじめるHeroku 2018」で,Herokuのチュートリアルをやりました.
チュートリアルではあらかじめ用意されていたアプリ使ったので,今回は自分で書いたPythonコードを使って,Herokuにデプロイしてみたいと思います.具体的には,「タイムズカープラスで最安プランを選ぶ」で書いたPythonコードを動かします1.
フレームワークはFlaskを使うことにします.
なぜFlask?
Flaskを使ったのは,以下のような思考によるものです.
- 何かフレームワークを使ってみたい
- でもシンプルなやつ,必要最小限で良い
- なんかFlaskってのが良いらしい
参考にさせていただいたQiita記事
作業前のローカル環境
- MacOSX 10.11.6
- Python 3.6.3
- HerokuCLI 7.7.10
- Git 2.10.1
1. 準備
まずは準備です.ローカル環境を作っていきます.
1.1. Pipenvで環境を作る
「PythonではじめるHeroku 2018」で学んだ通り,Pipenvを使います.
アプリ名はtcpopt
(Times Car Plus Optimization)にしておきましょう.
$ mkdir tcpopt
$ cd tcpopt
$ pipenv --three
これでPipfile
が作成されます.
1.2. Flaskインストール
Flaskをインストールします.あと,計算で使うPandasですね.
$ pipenv install flask
$ pipenv install pandas
この状態だと,Pipfile
はこんな感じです.
$ more Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
flask = "*"
pandas = "*"
[dev-packages]
[requires]
python_version = "3.6"
次はアプリの中身を作っていきます.
2. アプリの中身を作る
今回は(JavascriptもCSSも使わずに)フロントページで入力を促し,ボタンクリックで画面遷移して,遷移先で計算結果を表示する,というだけにしておきます.
2.1. Modelを作る
「タイムズカープラスで最安プランを選ぶ」でほぼベタ書きだったコードを,ちょっと整理しておきます.
料金テーブルを保持するクラスFareTable
と,条件に応じて料金を計算するクラスFareCalculator
を,1つのファイルmodel.py
にまとめます.
import math
import datetime as dt
import pandas as pd
class FareTable():
def __init__(self):
# plan names
plans = ['short', 'hour6', 'hour12', 'hour24', 'early', 'late', 'double']
# fare amount
a = {'base':[0, 4020, 6690, 8230, 2060, 2060, 2580],
'extra':[206, 206, 206, 206, 206, 206, 206],
'distance':[0, 0, 16, 16, 16, 16, 16]}
amount = pd.DataFrame(data=a, index=plans)
# fare condition
c = {'type':['length', 'length', 'length', 'length', 'time', 'time', 'time'],
'length':[0, 6, 12, 24, 6, 9, 15],
'start':['', '', '', '', 18, 0, 18],
'by':['', '', '', '', 24, 9, 24],
'max':[72, 6, 6, 6, 6, 6, 6]}
condition = pd.DataFrame(data=c, index=plans)
self.__whole = pd.concat([amount, condition], axis=1)
@property
def whole(self):
return self.__whole
class FareCalculator():
def __init__(self, start, end, distance):
self.s = start
self.e = end
self.d = distance
self.__f = FareTable()
@property
def fare_table(self):
return self.__f.whole
def __calc_extra(self, plan):
fare = self.__f.whole
if fare.at[plan, 'type'] == 'length':
extra = self.e - self.s - dt.timedelta(
hours=int(fare.at[plan, 'length']))
elif fare.at[plan, 'type'] == 'time':
plan_start = dt.datetime(year=self.s.year,
month=self.s.month,
day=self.s.day,
hour=fare.at[plan, 'start'])
plan_end = plan_start + dt.timedelta(
hours=int(fare.at[plan, 'length']))
extra = self.e - plan_end
else:
extra = math.inf
return max(0, math.ceil((extra.total_seconds()/3600)*4))
def calc_fare(self, plan):
fare = self.__f.whole
if (fare.at[plan, 'type'] == 'time') and (
self.s.hour < fare.at[plan, 'start'] or
self.s.hour >= fare.at[plan, 'by']):
return math.inf
else:
extra_qnum = self.__calc_extra(plan)
if extra_qnum > fare.at[plan, 'max']*4:
return math.inf
else:
return (fare.at[plan, 'base'] +
fare.at[plan, 'extra'] * extra_qnum +
fare.at[plan, 'distance'] * self.d)
2.2. フロントページを作る
見た目は考えずに作ります.template
ディレクトリの中に入れます.
<!doctype html>
<html>
<head>
<title>TCPOPT: Times Car Plus OPTimization</title>
</head>
<body>
<h1>TCPOPT: Times Car Plus OPTimization</h1>
<form method="post" action="/result">
from:
<input type="datetime-local" value="2018-08-26T19:30" name="start" /><br />
to:
<input type="datetime-local" value="2018-08-27T08:30" name="end" /><br />
distance:
<input type="number" value="200" min="0" max="500" step="1" name="distance" />km<br />
<button>Calculate</button>
</form>
</body>
</html>
2.3. Viewを作る
とりあえず書いていきます.
datetime-localをパースする部分はこちらを参考にしました.きっともっと良いやり方があるのでしょうが,今は細かいことは気にしません.
from flask import Flask, render_template, request
import datetime as dt
import pandas as pd
from model import FareCalculator, FareTable
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/result', methods=['POST'])
def result():
# need some improvement
start = request.form['start']
start = start.replace('T', '-').replace(':', '-').split('-')
start = [int(v) for v in start]
start = dt.datetime(*start)
end = request.form['end']
end = end.replace('T', '-').replace(':', '-').split('-')
end = [int(v) for v in end]
end = dt.datetime(*end)
distance =(request.form['distance'])
calculator = FareCalculator(start, end, distance)
plans = calculator.fare_table.index
result = pd.DataFrame(columns=[], index=plans)
for plan in plans:
result.at[plan, 'amount'] = calculator.calc_fare(plan)
return ('Plan:' + str(result.amount.idxmin())
+ ', Amount: ' + str(result.amount.min())
)
if __name__ == "__main__":
app.run(debug=True)
2.4. アプリを起動する
ここまででアプリは動くようになりました.ローカルで起動してみます.
$ pipenv shell
$ python view.py
この状態で,http://localhost:5000/をWEBブラウザで開くと,index.html
が表示されます.
そのままcalculateボタンを押すと,http://localhost:5000/resultにページ遷移し,計算結果
Plan:double, Amount: 5780.0
が表示されます.
また,利用開始時刻,返却予定時刻,距離を変更してボタンを押すと,計算結果も変わります.
ターミナルでCtrl+C
を押せば,サーバが停止します.
3. Herokuにデプロイする
最後にHerokuで動かします.
3.1. ローカルで動かす
gunicorn
をインストールします.
$ pipenv install gunicorn
次に,Procfile
を作ります.
web: gunicorn view:app
この時点で,ディレクトリ構成はこんな感じになっています.
$ tree -A
.
├── Pipfile
├── Pipfile.lock
├── Procfile
├── model.py
├── templates
│ └── index.html
└── view.py
Herokuを使ってローカルで動かします.
$ heroku local web
http://localhost:5000/をWEBブラウザで開くと,さっきFlaskで動かしたのと同じように,index.html
が表示されます.
3.2. Heroku上にデプロイする
まずはHerokuにログインして,アプリをcreate
します.名前が重複してなければできるはず.
$ heroku login
$ heroku create tcpopt
Gitのリポジトリを作ってpush
します.
$ git init
$ git add .
$ git commit -m "first commit"
$ git push heroku master
$ heroku open
これで,作成したWEBアプリ「TCPOPT: Times Car Plus Optimization」が動いているのが確認できます.
(開発中のものなので,止めたり消したりすることがあります.あと当然ながら計算結果の正確性は保証しません.テスト全くしてないし.)
おわりに
これでタイムズカープラスの最安プランがWEBで計算できるようになりました.
ただ,中身は粗だらけなので,これから改良していく予定です.