bottleアプリをuWSGIで動かす

やってみよう!

今回動かすアプリ

単純に、指定されたファイルをダウンロードさせるだけのアプリを書いた。

# encoding: utf-8

from bottle import route, static_file, abort, run, default_app
import os

# root

@route('/')
def index():
    abort(404, "File not found.")

# /path/to/file

@route('/')
def static(path):
    file_path = os.path.join("./storage", path)
    if os.path.isfile(file_path):
        return static_file(path, root='./storage', download=True)
    else:
        abort(404, "File not found.")

if __name__ == '__main__':
    run(host='localhost', port='8080', debug=True, reloader=True)
else:
    application = default_app()

run 関数が直接 python index.py したとき用の記述で、application = default_app() が uWSGI 用の記述。

下準備

uWSGI で動かす前に、実行用の環境を env という名前で作っておく。

takatoh@apostrophe $ ls
index.py  requirements.txt
takatoh@apostrophe $ virtualenv env
New python executable in env/bin/python
Installing setuptools, pip, wheel...done.
takatoh@apostrophe $ source env/bin/activate
(env)takatoh@apostrophe $ pip install -r requirements.txt
Collecting bottle==0.12.10 (from -r requirements.txt (line 1))
/home/takatoh/w/myapp/env/local/lib/python2.7/site-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
  Using cached bottle-0.12.10-py2-none-any.whl
Installing collected packages: bottle
Successfully installed bottle-0.12.10
/home/takatoh/w/myapp/env/local/lib/python2.7/site-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
You are using pip version 7.1.2, however version 9.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(env)takatoh@apostrophe $ deactivate

それから、storage という名前のディレクトリを作って、ファイル(101_ObjectCategories.tar.gz)を入れておく。これをダウンロードしてみようってわけだ。

uWSGIで起動

まずは、コマンドラインから起動してみる。

takatoh@apostrophe $ uwsgi --http :8080 --wsgi-file index.py -H env

ダウンロードできるか、テスト。

takatoh@apostrophe $ wget http://localhost:8080/101_ObjectCategories.tar.gz
--2016-11-24 20:33:35--  http://localhost:8080/101_ObjectCategories.tar.gz
localhost (localhost) をDNSに問いあわせています... 127.0.0.1
localhost (localhost)|127.0.0.1|:8080 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 131740031 (126M) [application/x-tar]
`101_ObjectCategories.tar.gz' に保存中

100%[======================================>] 131,740,031 68.1MB/s   時間 1.8s 

2016-11-24 20:33:37 (68.1 MB/s) - `101_ObjectCategories.tar.gz' へ保存完了 [131740031/131740031]

出来た。以前、Flask アプリを動かした時より簡単だな。

設定ファイルで起動

uWSGI 用の設定ファイル。

[uwsgi]
uid = takatoh
gid = takatoh
http = :8080
venv = /home/takatoh/w/myapp/env
wsgi-file = /home/takatoh/w/myapp/index.py
master=true
pidfile=/home/takatoh/w/myapp/myapp.pid
logger=file:/home/takatoh/w/myapp/myapp.log

起動。

takatoh@apostrophe $ uwsgi uwsgi.ini
[uWSGI] getting INI configuration from uwsgi.ini

ダウンロードのテスト。

takatoh@apostrophe $ wget http://localhost:8080/101_ObjectCategories.tar.gz
--2016-11-25 08:35:08--  http://localhost:8080/101_ObjectCategories.tar.gz
localhost (localhost) をDNSに問いあわせています... 127.0.0.1
localhost (localhost)|127.0.0.1|:8080 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 131740031 (126M) [application/x-tar]
`101_ObjectCategories.tar.gz' に保存中

100%[======================================>] 131,740,031 28.6MB/s   時間 4.5s 

2016-11-25 08:35:13 (27.8 MB/s) - `101_ObjectCategories.tar.gz' へ保存完了 [131740031/131740031]

OK。うまくいった!

辞書の値によってキーを削除する

例えば、こんな辞書があったとする。

>>> dic = {'Andy': 27, 'Bill': 32, 'Charlie': 24}

この辞書から、値が 30 を超えるキー(つまり ‘Bill’)を削除したい。こうすると:

>>> for k in dic:
...     if dic[k] > 30:
...         del(dic[k])
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

エラーになる(イテレーションの最中に辞書のサイズが変わってるって怒られる)。
でもキーは削除されてる。

>>> dic
{'Charlie': 24, 'Andy': 27}

なんでだかは解らない。
エラーが出ないようにするには items() メソッドを使えばいい。今度は値が 25 を超えるキーを削除してみる。

>>> for k, v in dic.items():
...     if v > 25:
...         del(dic[k])
...
>>> dic
{'Charlie': 24}

無事に消えた。

Pythonの軽量Webフレームワークbottleを使ってみる(2)

テンプレートについて補足。
昨日のスクリプトでは、template 関数の引数に直接テンプレートを文字列として渡していた。短いテンプレートならそれでもいいけど、引数として文字列で渡すには長すぎるのが普通だ。そこで、テンプレートそのものじゃなく、名前で指定できるようになっている。
テンプレートは、スクリプトと同じディレクトリの下に views という名前でディレクトリを作って、その中に入れておく。そうすると bottle がテンプレートを見つけて読み込んでくれる。
次のスクリプトでは info という名前のテンプレートを使っている。

# encoding: utf-8

from bottle import route, request, template, run

@route('/')
def index():
    return "<a href="\&quot;./info\&quot;">Link</a>"

@route('/info')
def info():
    ip = request.environ.get('REMOTE_ADDR')
    referer = request.headers.get('Referer') or ''
    return template("info", ip=ip, referer=referer)

run(host='localhost', port='8080', debug=True, reloader=True)

で、テンプレートのほうはこう。

<h1>Remote information</h1>
IP address: {{ip}}

Referer: {{referer}}

Pythonの軽量Webフレームワークbottleを使ってみる

インストール

pip でインストール。

takatoh@apostrophe $ pip install bottle
Collecting bottle
/usr/local/lib/python2.7/dist-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
  Downloading bottle-0.12.10-py2-none-any.whl (88kB)
    100% |████████████████████████████████| 90kB 3.6MB/s 
Installing collected packages: bottle
Exception:
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/pip/basecommand.py", line 211, in main
    status = self.run(options, args)
  File "/usr/local/lib/python2.7/dist-packages/pip/commands/install.py", line 311, in run
    root=options.root_path,
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_set.py", line 646, in install
    **kwargs
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_install.py", line 803, in install
    self.move_wheel_files(self.source_dir, root=root)
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_install.py", line 998, in move_wheel_files
    isolated=self.isolated,
  File "/usr/local/lib/python2.7/dist-packages/pip/wheel.py", line 339, in move_wheel_files
    clobber(source, lib_dir, True)
  File "/usr/local/lib/python2.7/dist-packages/pip/wheel.py", line 317, in clobber
    shutil.copyfile(srcfile, destfile)
  File "/usr/lib/python2.7/shutil.py", line 83, in copyfile
    with open(dst, 'wb') as fdst:
IOError: [Errno 13] Permission denied: '/usr/local/lib/python2.7/dist-packages/bottle.py'
You are using pip version 7.1.2, however version 9.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

って、えー、なんかいきなりエラーが出るじゃん。pip をアップグレードしろ、といってるみたいなので、そうしてみる。

takatoh@apostrophe $ pip install --upgrade pip
/usr/local/lib/python2.7/dist-packages/pip/_vendor/requests/packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
Collecting pip
  Downloading pip-9.0.1-py2.py3-none-any.whl (1.3MB)
    100% |████████████████████████████████| 1.3MB 451kB/s 
Installing collected packages: pip
  Found existing installation: pip 7.1.2
    Uninstalling pip-7.1.2:
Exception:
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/pip/basecommand.py", line 211, in main
    status = self.run(options, args)
  File "/usr/local/lib/python2.7/dist-packages/pip/commands/install.py", line 311, in run
    root=options.root_path,
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_set.py", line 640, in install
    requirement.uninstall(auto_confirm=True)
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_install.py", line 716, in uninstall
    paths_to_remove.remove(auto_confirm)
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_uninstall.py", line 125, in remove
    renames(path, new_path)
  File "/usr/local/lib/python2.7/dist-packages/pip/utils/__init__.py", line 315, in renames
    shutil.move(old, new)
  File "/usr/lib/python2.7/shutil.py", line 303, in move
    os.unlink(src)
OSError: [Errno 13] Permission denied: '/usr/bin/pip'
You are using pip version 7.1.2, however version 9.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

ダメじゃん。同じようなエラーが出る。
ググってみると↓このページを見つけた。

 cf. bottle超初歩入門 – Qiita

なんか、pip でインストールしようとするのが間違いみたいなことが書いてある。bottle はファイル1つでできているので、それをダウンロードして、スクリプトと同じディレクトリに置くのが簡単らしい。

https://raw.githubusercontent.com/bottlepy/bottle/master/bottle.py

takatoh@apostrophe $ wget https://raw.githubusercontent.com/bottlepy/bottle/master/bottle.py

これで bottle.py がダウンロードできた。なんか釈然としないけど今のところはよしとする。

Hello, world!

まず初めは、定石通り Hello, world。

from bottle import route, run

@route('/hello')
def hello():
    return "Hello, world!"

run(host='localhost', port='8080', debug=True)

bottle から routerun をインポート。route デコレータでルーティングを行い、run 関数でサーバを走らせる、ってことのようだ。
これで http://localhost:8080/hello にブラウザでアクセスすると、ちゃんと「Hello, world!」と表示された。

パス中にパラメータ

URL のパス中にパラメータを入れることができる。下の < > で囲んであるところがそれ。

from bottle import route, run

@route('/hello/')
def hello_with_name(name):
    return "Hello, " + name + "!"

run(host='localhost', port='8080', debug=True)

例えば、http://localhost:8080/hello/Andy にアクセスすると「Hello, Andy!」と返ってくる。

リダイレクト

redirect 関数を使う。

from bottle import route, redirect, run

@route('/')
def index():
    redirect('/hello')

@route('/hello')
def hello():
    return "Hello, world!"

run(host='localhost', port='8080', debug=True, reloader=True)

ついでに、run 関数の引数に reloader=True を指定した。これによって、サーバを起動したままでもファイルを書き換えると自動的にリロードしてくれるようになる。

Not found

abort 関数。ステータスコードとメッセージを指定できる。

from bottle import route, redirect, abort, run

@route('/')
def index():
    redirect('/hello')

@route('/hello')
def hello():
    abort(404, "Not Found.")

run(host='localhost', port='8080', debug=True, reloader=True)

この例では、http://localhost:8080/ にアクセスすると /hello にリダイレクトされて、Not found になる。

リファラと接続元IPアドレス

request オブジェクトからリクエストの情報が取れる。リファラは request.headers.get('Referer')

# encoding: utf-8

from bottle import route, request, run

@route('/')
def index():
    return "<a href="\&quot;./ref\&quot;">Link</a>"

@route('/ref')
def ref():
    referer = request.headers.get('Referer')
    return referer

run(host='localhost', port='8080', debug=True, reloader=True)

一方、IPアドレスは request.environ.get('REMOTE_ADDR')

# encoding: utf-8

from bottle import route, request, run

@route('/my_ip')
def my_ip():
    ip = request.environ.get('REMOTE_ADDR')
    return ip

run(host='localhost', port='8080', debug=True, reloader=True)

テンプレート

bottle から template をインポートすることで、簡易なテンプレートエンジンが使えるようになる。

# encoding: utf-8

from bottle import route, request, template, run

@route('/')
def index():
    return "<a href="\&quot;./ref\&quot;">Link</a>"

@route('/ref')
def ref():
    ip = request.environ.get('REMOTE_ADDR')
    referer = request.headers.get('Referer') or ''
    return template("Your IP is {{ip}}, referer is {{referer}}.", ip=ip, referer=referer)

run(host='localhost', port='8080', debug=True, reloader=True)

静的ファイル

static_file 関数を使う。

# encoding: utf-8

from bottle import route, static_file, run

@route('/')
def static(file_path):
    return static_file(file_path, root='./storage', download=True)

run(host='localhost', port='8080', degug=True, reloader=True)

引数に download=True を指定すると、ブラウザで開くのではなくダウンロードするようになる。

ふう、今日のところはここまで。

[追記]

Windows 10 では、pip で普通にインストールできた。なんでだ?

[追記2]

sudo をつけたら、pip のアップグレードも bottle のインストールもうまくいった。

clickで–versionオプションを実装する

以前使った Python の click モジュールだけど、コマンドやオプションを定義すると自動で --help オプションを作ってくれる。例えばこんなスクリプトがあったとすると:

# encoding: utf-8

import click

@click.command()
@click.option('--times', '-t', type=int, default=1, help='Repeat.')
@click.option('--morning', '-m', is_flag=True, help='In morning.')
@click.argument('name', default='World')
def main(times, morning, name):
    if morning:
        greeting = 'Good morning'
    else:
        greeting = 'Hello'
    for i in range(times):
        print '{greeting}, {name}!'.format(greeting=greeting, name=name)

if __name__ == '__main__':
    main()

こんな感じでヘルプメッセージを生成してくれる。

takatoh@nightschool $ python hello.py --help
Usage: hello.py [OPTIONS] [NAME]

Options:
  -t, --times INTEGER  Repeat.
  -m, --morning        In morning.
  --help               Show this message and exit.

だけど、--version オプションまでは作ってくれない。まあ、当たり前だ。
で、どうするかというと @click.version_option デコレータを使う。

# encoding: utf-8

import click

script_version = '0.1.0'

@click.command()
@click.option('--times', '-t', type=int, default=1, help='Repeat.')
@click.option('--morning', '-m', is_flag=True, help='In morning.')
@click.version_option(version=script_version)
@click.argument('name', default='World')
def main(times, morning, name):
    if morning:
        greeting = 'Good morning'
    else:
        greeting = 'Hello'
    for i in range(times):
        print '{greeting}, {name}!'.format(greeting=greeting, name=name)

if __name__ == '__main__':
    main()

これで --version オプションが使えるようになる。

takatoh@nightschool $ python hello_version.py --version
hello_version.py, version 0.1.0

ちょっと表示が冗長なので、もう少しシンプルにしてみる。デコレータの引数でメッセージのフォーマットを渡してやる。

# encoding: utf-8

import click

script_version = '0.1.0'

@click.command()
@click.option('--times', '-t', type=int, default=1, help='Repeat.')
@click.option('--morning', '-m', is_flag=True, help='In morning.')
@click.version_option(version=script_version, message='%(prog)s v%(version)s')
@click.argument('name', default='World')
def main(times, morning, name):
    if morning:
        greeting = 'Good morning'
    else:
        greeting = 'Hello'
    for i in range(times):
        print '{greeting}, {name}!'.format(greeting=greeting, name=name)

if __name__ == '__main__':
    main()
takatoh@nightschool $ python hello_version2.py --version
hello_version2.py v0.1.0

ちょっとだけシンプルになった。

参考ページ:
 cf. API – click

pyenvを使って最新のPythonをインストールする

pyenvとは

複数のバージョンの Python を切り替えて使えるようにするもの。Ruby の rvm とか rbenv みたいなもの。

pyenvのインストール

インストールというか、GitHub からクローンしてくる。ここではホームディレクトリ下の .pyenv にクローン。

takatoh@nightschool $ git clone git://github.com/yyuu/pyenv.git .pyenv
Cloning into '.pyenv'...
remote: Counting objects: 11571, done.
remote: Compressing objects: 100% (158/158), done.
remote: Total 11571 (delta 107), reused 0 (delta 0), pack-reused 11391
Receiving objects: 100% (11571/11571), 2.05 MiB | 399.00 KiB/s, done.
Resolving deltas: 100% (8114/8114), done.
Checking connectivity... done.

続いて .bashrc に以下を追記。

#for pyenv
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

シェルを起動し直すと pyenv コマンドが使えるようになる。

takatoh@nightschool $ pyenv
pyenv 20151210-1-ge66dcf2
Usage: pyenv <command> [<args>]

Some useful pyenv commands are:
   commands    List all available pyenv commands
   local       Set or show the local application-specific Python version
   global      Set or show the global Python version
   shell       Set or show the shell-specific Python version
   install     Install a Python version using python-build
   uninstall   Uninstall a specific Python version
   rehash      Rehash pyenv shims (run this after installing executables)
   version     Show the current Python version and its origin
   versions    List all Python versions available to pyenv
   which       Display the full path to an executable
   whence      List all Python versions that contain the given executable

See `pyenv help ' for information on a specific command.
For full documentation, see: https://github.com/yyuu/pyenv#readme

Python 2.7.11をインストール

特定のバージョンをインストールするには pyenv install コマンド。

takatoh@nightschool $ pyenv install 2.7.11
Downloading Python-2.7.11.tgz...
-> https://www.python.org/ftp/python/2.7.11/Python-2.7.11.tgz
Installing Python-2.7.11...
WARNING: The Python bz2 extension was not compiled. Missing the bzip2 lib?
Installed Python-2.7.11 to /home/takatoh/.pyenv/versions/2.7.11

あれ、なんか WARNING が出た。インストール自体はできてるようだけど……bzip2 が足りないようだ。このページを参考に bzip2 をインストール。

takatoh@nightschool $ sudo apt-get install -y bzip2
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
bzip2 はすでに最新版です。
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
  ax25-node firefox-locale-en libax25 linux-headers-3.13.0-34
  linux-headers-3.13.0-34-generic linux-image-3.13.0-34-generic
  linux-image-extra-3.13.0-34-generic openbsd-inetd
これを削除するには 'apt-get autoremove' を利用してください。
アップグレード: 0 個、新規インストール: 0 個、削除: 0 個、保留: 21 個。

インストールされてるじゃん。なんで WARNING が出たんだ?
まあいい、このまま進めよう。

Python 3.5.1をインストール

Python3 系の最新バージョンをインストール。

takatoh@nightschool $ pyenv install 3.5.1
Downloading Python-3.5.1.tgz...
-> https://www.python.org/ftp/python/3.5.1/Python-3.5.1.tgz
Installing Python-3.5.1...
WARNING: The Python bz2 extension was not compiled. Missing the bzip2 lib?
Installed Python-3.5.1 to /home/takatoh/.pyenv/versions/3.5.1

shimのリフレッシュ

shim がなんだかわからないけど、参考にしたページに書いてあったのでやっておく。

takatoh@nightschool $ pyenv rehash

Pythonのバージョンの切り替え

ここまでの作業で、複数のバージョンがインストールできた。利用可能なバージョンは pyenv versions コマンドで確認できる。

takatoh@nightschool $ pyenv versions
* system (set by /home/takatoh/.pyenv/version)
  2.7.11
  3.5.1
takatoh@nightschool $ python -V
Python 2.7.6

今はシステム標準の 2.7.6。これを 3.5.1 に切り替えるには次のようにする。

takatoh@nightschool $ pyenv global 3.5.1
takatoh@nightschool $ pyenv versions
  system
  2.7.11
* 3.5.1 (set by /home/takatoh/.pyenv/version)

バージョンの切り替えには、もうひとつ pyenv local コマンドもある。どう違うのかよくわからない。

ともかく、これでバージョンを切り替えて使えるようになった。

[追記]

参考ページ
 cf. Macでpyenv+virtualenv – Qiita

Pythonでは空文字列は偽

空文字列は偽で、それ以外の文字列は真と評価される。忘れてたよ。
せっかくなので確認しておこう。

  • 空文字列(””)は偽、それ以外は真
  • 数値の 0 は偽、それ以外は真
  • 空の配列は偽、それ以外は真
  • 空の辞書は偽、それ以外は真
  • 空のタプルは偽、それ以外は真

これ以外では、True は真、False と None は偽。

確認してみよう。

>>> def true_or_false(obj):
...     if obj:
...         return True
...     else:
...         return False
... 
>>> true_or_false("")
False
>>> true_or_false("a")
True
>>> true_or_false(0)
False
>>> true_or_false(-1)
True
>>> true_or_false([])
False
>>> true_or_false([1])
True
>>> true_or_false({})
False
>>> true_or_false({'a':'andy'})
True
>>> true_or_false(())
False
>>> true_or_false((1,))
True

Pythonの辞書から値がNoneであるキーを削除する

Ruby では Hash#reject を使えば簡単にできるのに、Python には使えそうなメソッドが見当たらない。

>>> def delete_if_none(dic):
...     for k, v in dic.items():
...         if v is None:
...             del(dic[k])
...     return dic
... 
>>> dic
{'a': 'Andy', 'c': 'Charlie', 'b': None}
>>> dic2 = delete_if_none(dic)
>>> dic2
{'a': 'Andy', 'c': 'Charlie'}
>>> dic
{'a': 'Andy', 'c': 'Charlie'}

JSONをPOSTする

昨日のエントリではクライアントを Ruby で書いたけども、サーバ側が Python(Flask) なのでやっぱりクライアントも Python がいい。そういうわけで JSON を POST するのにいいライブラリがないかとググってみたら、requests というのが良さそうだ。

 cf. Requests: 人間のためのHTTP

で、書いてみたのがこれ:

#!/usr/bin/env python
# encoding: utf-8

import sys
import click
import yaml
import csv
import requests

def post_book(data, uri_base):
    post_data = {
        'title' : data['title'],
        'volume' : data['volume'] or '',
        'series' : data['series'] or '',
        'series_volume' : data['series_volume'] or '',
        'author' : data['author'] or '',
        'translator' : data['translator'] or '',
        'publisher' : data['publisher'] or '',
        'category' : data['category'] or 'その他',
        'format' : data['format'] or 'その他',
        'isbn' : data['isbn'] or '',
        'published_on' : data['published_on'] or '',
        'original_title' : data['original_title'] or '',
        'note' : data['note'] or '',
        'keyword' : data['keyword'] or '',
        'disk' : data['disk'] or '',
        'disposed' : data['disposed'] or '0'
    }
    uri = uri_base + 'api/book/add/'
    res = requests.post(uri, json=post_data)
    print title_with_vol(res.json()['books'][0])

def title_with_vol(book):
    if book['volume'] == '':
        return book['title']
    else:
        return book['title'] + ' [' + book['volume'] + ']'

def load_yaml(yamlfile):
    f = open(yamlfile, 'r')
    data = yaml.load(f)
    f.close()
    return data['books']

def load_csv(csvfile):
    f = open(csvfile, 'r')
    reader = csv.DictReader(f)
    data = []
    for row in reader:
        data.append(row)
    f.close()
    return data

@click.group()
@click.pass_context
@click.option('--repository', '-R', help='Specify repository.')
def cmd(ctx, repository):
    if repository is None:
        raise click.BadParameter('--repository option is required.')
    ctx.obj['repository'] = repository.rstrip('/') + '/'

@cmd.command(help='Post books to Bruschetta.')
@click.pass_context
@click.option('--csv', is_flag=True, help='Input from CSV.')
@click.argument('input')
def post(ctx, csv, input):
    if csv:
        books = load_csv(input)
    else:
        books = yaml_load(input)
    for book in books:
        post_book(book, ctx.obj['repository'])

def main():
    cmd(obj={})


main()

ついでに、サブコマンドとオプションの処理には click というライブラリを使った。click についてはココらへんが詳しい。

 cf. Python: コマンドラインパーサの Click が便利すぎた – CUBE SUGAR CONTAINER
 cf. click

実行例:

takatoh@nightschool $ python bin/post_book.py --repository http://localhost:5000/ post --csv books.csv
ウルフ・ウォーズ
世界の涯ての夏
冴えない彼女の育てかた [9]

うまくいった。

FlaskでJSONを受け取る

Flask で POST された JSON を受け取るには request.json を使えばいい。
サーバー側(Flask)のコードはこんな感じ:

from flask import request, redirect, url_for, render_template, flash, json

(中略)

@app.route('/api/book/add/', methods=['POST'])
def api_book_add():
    categ = Category.query.filter_by(name=request.json['category']).first()
    fmt = Format.query.filter_by(name=request.json['format']).first()
    book = Book(
        title = request.json['title'],
        volume = request.json['volume'],
        series = request.json['series'],
        series_volume = request.json['series_volume'],
        author = request.json['author'],
        translator = request.json['translator'],
        publisher = request.json['publisher'],
        category_id = categ.id,
        format_id = fmt.id,
        isbn = request.json['isbn'],
        published_on = request.json['published_on'],
        original_title = request.json['original_title'],
        note = request.json['note'],
        keyword = request.json['keyword'],
        disk = request.json['disk']
    )
    if request.json['disposed'] == '1':
        book.disposed = True
    db.session.add(book)
    db.session.commit()
    return json.dumps({ "status": "OK", "books": [book.to_dictionary()]})

POST するクライアント側(こっちは Ruby)はこう:

#!/usr/bin/env ruby
# encoding: utf-8

require 'httpclient'
require 'yaml'
require 'csv'
require 'json'
require 'optparse'

def post_book(book)
  post_url = "http://localhost:5000/api/book/add/"
  post_data = {
    "title"          => book["title"],
    "volume"         => book["volume"]         || "",
    "series"         => book["series"]         || "",
    "series_volume"  => book["series_volume"]  || "",
    "author"         => book["author"]         || "",
    "translator"     => book["translator"]     || "",
    "publisher"      => book["publisher"]      || "",
    "category"       => book["category"]       || "その他",
    "format"         => book["format"]         || "その他",
    "isbn"           => book["isbn"]           || "",
    "published_on"   => book["published_on"]   || "",
    "original_title" => book["original_title"] || "",
    "note"           => book["note"]           || "",
    "keyword"        => book["keyword"]        || "",
    "disk"           => book["disk"]           || "",
    "disposed"       => book["disposed"]       || "0"
  }
  json_data = post_data.to_json

  res = @client.post_content(post_url, json_data, "Content-Type" =&gt; "application/json")
  result = JSON.parse(res)
  puts title_with_vol(result["books"].first)
end

def title_with_vol(book)
  if book["volume"].nil? || book["volume"].empty?
    book["title"]
  else
    book["title"] + " [" + book["volume"] + "]"
  end
end

options = {}
opts = OptionParser.new
opts.banner = <<EOB

Options:
EOB
opts.on("--csv", "Input from CSV."){|v| options[:csv] = true }
opts.on_tail("--help", "-h", "Show this message"){|v|
  puts opts.help
  exit(0)
}
opts.parse!

@client = HTTPClient.new

inputfile = ARGV.shift
books = if options[:csv]
  csvfile = File.open(inputfile, "r")
  CSV.new(csvfile, headers: true)
else
  YAML.load_file(inputfile)["books"]
end
books.each do |book|
  post_book(book)
end

HTTPClient#post_content に JSON と "Content-Type" => "application/json" を渡してるのがミソ。Content-Typeapplication/json になってることで、Flask.request.json でデコードされたデータが得られる。