Python: Tornadoの非同期でアプリを書くのをあきらめたはなし

 最近GoogleAppEngineで動かしてたアプリをDockerコンテナへ移植した。そのフレームワークとしてノンブロッキングな非同期処理ができるTornadoを選んだのだが、書いている途中で非同期で書くのがしんどくなって同期に切り替えた。どのあたりがしんどかったのかをメモとして残しておく。

 Tornadoでは非同期で値を返すメソッドはreturnではなくyieldを使うようになっている。ジェネレータで使っていたyieldである。これがイマイチ非同期処理を行っているのかなんなのかキーワードとしてわかりにくかった。
 たとえばC#にはawaitというキーワードがあり、これが非同期処理の結果を待つことを示す。awaitはそのために作られた機能で、キーワードが示すのは一意の機能である。それと比較するとTornadoを使った場合ではちょっとだけコードが読みづらかった。

 そしてTornadoで書いていて、非同期処理になっていないのにyieldキーワードを使っているよ、とエラーを返されたが該当する行を示してくれなかった。これは処理を追っていけばどこがエラーを吐いているか把握できるのだが、そもそもふつうにやってればエラーを吐いた行なんて、苦労せずともエラー出力で示してくれるものである。そういうものをつぶしてまで非同期処理を選択する必要はなかったので、同期処理に切り替えてデバッグをシコシコとやっていった。同期はデバッグしやすい。

 要は今回移植したアプリが、わりとふつうのコードであることが自分の中では大事なことに気づいたので、ノンブロッキングな非同期をやめて同期で書いていった。もし今後非同期が必要になったら、個人的にはその場合はASP.NET MVCでいきたいと感じた。
 
comment: 0

Python: Tornadoアプリでログイン済みユーザへの対応をするハンドラ

 TornadoでHelloWorldをやってくれるリクエストハンドラを書くと下記である。
class MainHandler(tornado.web.RequestHandler):

def get(self):
self.write("HelloWorld")


 アプリに認証機能を入れたとき、ハンドラごとに認証チェックを書くのは面倒である。そこでgetメソッドやpostメソッド内に認証チェックを自分でいちいち書いていかなくても、自動でやってくれるリクエストハンドラを継承で作った。

class AuthBaseHandler(tornado.web.RequestHandler):

def prepare(self):
self.auth()

def auth(self):
auth_token = self.get_cookie("markofcain")
if not auth_token:
raise tornado.web.HTTPError(401)

db = self.settings["db"]
doc = db.session.find_one({"_id":auth_token})
if doc is None:
raise tornado.web.HTTPError(401)
else:
self.user = doc
return True


 あとはまたこれを継承すれば目的の、認証チェックをリクエストごとにやってくれるリクエストハンドラができる。

class HelloHandler(AuthBaseHandler):

def get(self):
return "Hello " + self.user["username"]


 アドベントカレンダー二日目にして早速うすうすな記事になった。突発忘年会。
comment: 0

PythonアプリにGithubのOAuthを使う

 Web上に置いている自分の読書メモがある。これの環境移植を最近行ったのだが、せっかくなので認証をGithubのOauthを使うようにしてみた。その流れ。

 まずGithubでアプリを登録する。個人設定を開く。


 メニューからOAuthアプリを選ぶ。


 新しくアプリの認証を通す。


 必要な情報の入力。


 一覧にもどってアプリの認証情報をメモ。必要なのはClientIDとClientSecret。


 ここまでで事前準備は終了で、あとのやることはアプリ側。
 OAuthでの認証の流れは…
1.自サイトのリンクからGithub(あるいはOAuthを持っているほかのサイト)へ行ってもらう(ワンクリック)
2.Githubで、自サイトがそこの認証情報を使わせてもらうことをユーザーに許諾してもらう(ワンクリック)
3.Githubであらかじめ設定しておいたリダイレクト先URIにユーザーが認証コードを持ってリダイレクトされてくる
4.自サイトサーバが認証コードを使ってGithubにトークンを要求
5.トークンが帰ってきたらそれを使って再度自サイトサーバがGithubへ、今度はユーザーの個人認証情報を要求
6.Githubから自サイトサーバが受け取った個人認証情報で認証処理を完了させる
という流れ。
 自サーバが持つべきなのはGithubの指定の認証ページへのリンクと、そこからリダイレクトで帰ってきたときの処理をするハンドラの二つか。
 というわけでリダイレクトでGithubから帰ってきたユーザーの認証処理を行うハンドラの流れを書いておく。

 まず認証コードを使ってトークン取得。
        code = self.get_argument("code", None)

client_id = GITHUB_CLIENT
if code:
url = "https://github.com/login/oauth/access_token"
secret = GITHUB_SECRET
data = {
"code": code,
"client_id": client_id,
"client_secret": secret
}
p_data = urllib.parse.urlencode(data).encode(encoding='ascii')
with urllib.request.urlopen(url, data=p_data) as f:
res = f.read().decode("ascii")
token = res.replace("access_token=", "").replace("&scope=&token_type=bearer", "")


 レスポンスからトークンを抜粋する方法がアレなのは置いておく。次はトークンを使って個人情報をGithubからもらう。
            url = "https://api.github.com/user?access_token=" + token

with urllib.request.urlopen(url) as f:
res = f.read().decode("ascii")

info = json.loads(res)


 JSONで返ってくるので、そこからloginIDなどを参照する。OAuthのためのGithubサーバとのやり取りはこれでひと段落。あとはその情報で認証を与えられるなら処理を続行など。
 以下に認証コードを持って帰ってきたユーザーに認証を与える処理の一連を。
class OauthHandler(tornado.web.RequestHandler):

def get(self):
code = self.get_argument("code", None)
client_id = GITHUB_CLIENT
if code:
url = "https://github.com/login/oauth/access_token"
secret = GITHUB_SECRET
data = {
"code": code,
"client_id": client_id,
"client_secret": secret
}
p_data = urllib.parse.urlencode(data).encode(encoding='ascii')
with urllib.request.urlopen(url, data=p_data) as f:
res = f.read().decode("ascii")
token = res.replace("access_token=", "").replace("&scope=&token_type=bearer", "")

url = "https://api.github.com/user?access_token=" + token
with urllib.request.urlopen(url) as f:
res = f.read().decode("ascii")

info = json.loads(res)
user_id = info["login"]

db = self.settings["db"]
doc = db.user.find_one({"_id":user_id})

if doc is not None:
auth_token = self.generate_token()
dt = datetime.datetime.utcnow()
db.session.insert({"_id":auth_token, "createdAt":dt})
self.set_cookie("markofcain", auth_token, secure=True)
self.redirect("/")
else:
self.write("failed")

else:
url = "https://github.com/login/oauth/authorize?client_id=" + client_id
self.write("<a href='{0}'>Oauth auth</a>".format(url))

def generate_token(self):
""" Generate a 32-char alnum string. 190 bits of entropy. """
alnum = ''.join(c for c in map(chr, range(256)) if c.isalnum())
return ''.join(random.choice(alnum) for _ in range(32))
comment: 0

ArukasにTornadoアプリのコンテナを上げるまで

 さくらのArukasにPythonのTornadoフレームワークを使ったアプリのコンテナを上げようと思った。なのでHelloWorld程度のものを実際に上げるまでをメモとして。


 まずはDockerイメージを作る環境が必要なのでその準備から。ぼくはつい最近までWindowsでDockerを使っていたが、ネットワークの不調からちょっとそれが面倒になった。なのでLinuxMintにDockerを入れたものを開発環境として用意した。DockerのLinuxMintへのインストールは下記コマンドで一発。
> sudo curl -sSL https://get.docker.com/ | sh


 Dockerを用意したらつづいて肝心のWebアプリ。Tornadoでサクッとしたものを用意した。あとでArukasの管理画面からDB接続文字列を入れるので、テストに使うハンドラはありきたりなHelloWorldメッセージではなく、環境変数を読み込んで出力するように。
import os


import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write(os.environ['db_conn'])

def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])


settings = {
"static_path": os.path.join(os.path.dirname(__file__), "static"),
}

def main():
app = make_app()
app.listen(80)
tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':
main()


 アプリが用意できたら次はDockerfile。Webアプリのプロセスはsupervisorを使ってデーモン管理。そうすれば自動で再起動設定とかもできる。
FROM ubuntu


RUN apt-get update
RUN apt-get install -y supervisor && \
apt-get install -y python3-pip
RUN pip3 install motor && \
pip3 install tornado

COPY . /usr/
WORKDIR /usr/

EXPOSE 80
CMD /usr/bin/supervisord -c supervisord.conf


 ついでにsupervisor.conf。これは最低限なので必要に応じて追記。
[supervisord]

nodaemon = true

[program:demoniacmadness]
command = python3 demon.py


 ここまできたらDockerfileに従ってDockerイメージをビルド・・・する前にDocker Hubへ行ってアカウントを取得しておく。イメージの名前に自分のアカウント名を入れる必要があるので。
https://hub.docker.com

 Docker Hubアカウントを入手したら改めてイメージ作成。Dockerfileのあるディレクトリで下記コマンド。
> docker build -t yourAcountName/imageName ./

 Dockerイメージの作成が終わったらそれをHubへプッシュする。まずログイン。
> docker login

 ログインできたらプッシュ。
> docker push yourAcountName/imageName

 あとはArukasで設定するだけ。imageに↑でプッシュしたyourAcountName/imageNameを入れる。最初のほうで書いたWebアプリが環境変数を読み込むものにしておいたので、環境変数を任意のキー、値で設定。その他必要事項をポイポイ入れて保存、そして再生ボタンでデプロイ開始。

アプリ名などがアレなのは当時読んでいたキルケゴールの本から取ったことによる。

 デプロイがこれを書いている時点で相当なネックだった。混雑からか4時間ぐらいトライしていたが全然成功しなかった。ただデプロイが失敗したことがわかるだけなので、混雑によるものかDockerイメージ内の設定ミスかもわからないし。まあまだ無料だし有料化するころにはそういうクオリティになってるだろう。

 デプロイがうまくいったらブラウザでアクセスしてみる。


 Webアプリのコンテナの土台ができたので、ここにぼくの必要な機能を実装していく。
comment: 0