Flaskをさわってみる

軽量なWebフレームワークが使えたら便利な場面があったので、最近ちょこちょこ名前を聞くようになったFlaskをテスト。
軽くさわっただけですが、かなり好印象を受けました。使えそうな雰囲気です。

成果物

あまり良いサンプルではないですが、ゲームのスクリプトと音声ファイルから、あるキャラクターをずーっと喋らせるというもの。真紅ー



サーバ側はこれだけです。

# -*- coding:utf-8 -*-

__author__="rubyu"
__date__ ="$2011/09/14 11:25:52$"

import unittest
import logging
logging.basicConfig(
    level=logging.DEBUG,
    format="%(levelname)-8s %(module)-16s %(funcName)-16s@%(lineno)d - %(message)s"
)

import os
import sys
import re
import json

from flask \
    import Flask, make_response, render_template, request
app = Flask(__name__)

from sekai.voice_parser import Voice
from sekai.script_parser import Script
from config import Config

voice = None
script = None

@app.route("/")
def show_root():
    return render_template("index.html")

@app.route("/query/json")
def query_json():
    u"""
    真紅の発言について、キーワードが含まれるものだけを返す。
    
    jsonで、以下の形式で。
    [ [発言, ボイスID], ... ]
    """
    keyword = request.args.get("keyword", "")
    logging.debug("keyword: %s", keyword)
    arr = []
    for text in script.texts:
        str, name, vid = text
        if name == u"真紅" and \
           -1 != str.find(keyword):
            str = html_ruby(str)
            arr.append((str, vid))
    logging.debug("size: %s", len(arr))
    response = make_response()
    response.data = json.dumps(arr)
    response.headers["Content-Type"] = "application/json"
    return response

@app.route("/voice/ogg/<voice_id>")
def output_voice(voice_id):
    u"""
    指定のIDのボイスを切り出して返す。
    形式はもとのoggのまま。
    """
    logging.debug("voice id: %s", voice_id)
    response = make_response()
    response.data = voice.get(voice_id)
    response.headers["Content-Type"] = "audio/ogg"
    response.headers["Content-Disposition"] = "attachment; filename=%s.ogg" % voice_id
    return response

ruby_pat = re.compile("\[(.+?)\|(.+?)\]")
def html_ruby(str):
    u"""
    ...[ルビ|テキスト]...の形式を、HTMLのルビタグに変換する。
    複数個含まれる場合は全て変換する。
    """
    return ruby_pat.sub(r"<ruby>\2<rt>\1</rt></ruby>", str)


class ProcWrapper(object):
    u"""
    コマンドラインから呼び出されるラッパ。
    テスト時はダミーに差し替えられる。
    """
    def app_run(self, voice_path, script_path):
        global voice
        global script
        try:
            voice = Voice.restore("voice.p")
        except Exception, e:
            logging.debug("Excepted: %s", e)
            voice = Voice(voice_path)
            voice.save("voice.p")
        try:
            script = Script.restore("script.p")
        except Exception, e:
            logging.debug("Excepted: %s", e)
            script = Script(script_path)
            script.save("script.p")
        app.run()
            

class OptionParserTestCase(unittest.TestCase):
    u"""
    コマンドラインオプションのテスト。
    """
    class DummyProcWrapper(object):
        u"""
        ダミーのラッパクラス。
        コールされた関数のログを持つ。
        """
        def __init__(self):
            self._log = []

        def __getattr__(self, name):
            def _dummy(self, *args, **kwargs):
                pass
            self._log.append(name)
            return _dummy    
        
        
    @classmethod
    def setUpClass(cls):
        cls._path = "/tmp/sekai"
        cls._argv = sys.argv
    
    @classmethod
    def terDownClass(cls):
        sys.argv = cls._argv
        
    def setUp(self):
        self._wrapper = self.DummyProcWrapper()
        sys.argv = [self._argv[0]]
        
    def test_path(self):
        u"""
        引数pathが与えられた場合。
        """
        sys.argv.append("--path=%s" % self._path)
        parse(self._wrapper)
        self.assertEqual(["app_run"], self._wrapper._log)
        

def parser():
    u"""
    OptionParserのインスタンスを返す。
    """
    from optparse import OptionParser
    p = OptionParser("usage: shinku_player.py --path=game_installed")
    p.add_option(
        "-p", 
        "--path",
        type="string",
        help="directory that the game installed"
    )
    return p

def parse(wrapper):
    u"""
    コマンドラインオプションで分岐し、ラッパをコールする。
    """
    p = parser()
    options, args = p.parse_args()
    
    if options.path:
        config = Config()
        config.path = options.path
        config.save("config.p")
    
    try:
        config = Config.restore("config.p")
    except Exception, e:
        logging.debug("Excepted: %s", e)
        p.error("Program has not enough data. The 'path' parameter never had been given to this program!")
        sys.exit()
        
    voice_path = os.path.join(config.path, "voice.bin")
    script_path = os.path.join(config.path, "World.hcb")
    if not os.path.isdir(config.path) or \
       not os.path.isfile(voice_path) or \
       not os.path.isfile(script_path):
        p.error("File not found. 'voice.bin' and 'World.hcb' must be in the 'path=%s'!" % config.path)
        sys.exit()
    
    app.debug = True
    wrapper.app_run(voice_path, script_path)
    
if __name__ == "__main__":
    debug = False
#    debug = True
    
    if debug:
        unittest.main()
    else:
        parse(ProcWrapper())