このBlogは移転しました。今後は aish.dev を御覧ください。

API star - Python3用 Web API framework

最近何度か名前を目にした Webアプリケーションフレームワーク API Star を試してみた

github.com

まだ開発中のフレームワークだが、Pythonの型アノテーションをうまく利用して、Web APIを簡単に開発できるようになっている。

インストール

githugからソースをダウンロードして実行してみた

$ git clone https://github.com/encode/apistar.git
$ pip install -e .

アプリケーションの作成

apistar コマンドでひな形を作成する。

$ apistar new test
test/app.py
test/tests.py

アプリケーションの実行

作成した app.py を実行する。

$ python app.py start
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
 * Restarting with fsevents reloader
 * Debugger is active!
 * Debugger PIN: 233-378-852

APIの追加

app.py を次のように修正する

from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls
from apistar import typesystem

def welcome(name=None):
    if name is None:
        return {'message': 'Welcome to API Star!'}
    return {'message': 'Welcome to API Star, %s!' % name}


# ここから追加
class IntProp(typesystem.Integer):
    minimum = 1
    maximum = 5

class EnumProp(typesystem.Enum):
    enum = ['one', 'two', 'three']

class RetType(typesystem.Object):
    properties = {
        'strprop': typesystem.string(max_length=100),
        'intprop': IntProp,
        'enumprop': EnumProp,
    }

def api1(intprop: IntProp, enumprop: EnumProp) -> RetType:
    return RetType(strprop='abc', intprop=intprop, enumprop=enumprop)

# ここまで追加


routes = [
    Route('/', 'GET', welcome),
    Route('/api1', 'GET', api1),   # この行を追加
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]

app = App(routes=routes)


if __name__ == '__main__':
    app.main()

この例では、2つのデータ型を定義して、関数 api1() の引数として指定している。一つは 1 から 5 までの整数値を表す型で、もう一つは文字列のone, two, three のいずれかの文字列の列挙型だ。

app.py を再起動し、ブラウザで http://127.0.0.1:8080/docs/# を開く。

f:id:atsuoishimoto:20170912083047p:plain

作成した APIが表示され、INTERACT をクリックするとAPIを実行できる。

f:id:atsuoishimoto:20170912083326p:plain

Webページの作成

APIだけではなく、Jinjaテンプレートを使って通常のHTMLも作成できる。

def generate_htmlpage(name, templates: Templates):
    templ = templates.get_template('hello.html')    # templates/hello.htmlを使用
    return templ.render(username=name)

RDBの利用

RDBへのアクセス手段として、SQLAlchemyとDjango-ORMを使用できる。

from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

from apistar.backends import sqlalchemy_backend

Base = declarative_base()

# Testテーブルの定義
class Test(Base):
    __tablename__ = "Test"
    id = Column(Integer, primary_key=True)
    name = Column(String)


def create_test(session: sqlalchemy_backend.Session, name: str):
    # Testテーブルにデータを登録し、idを返す
    test = Test(name=name)
    session.add(test)
    session.flush()
    return {'id': test.id}

routes = [
    Route('/create_test', 'GET', create_test),
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]

# Configure database settings.
settings = {
    "DATABASE": {
        "URL": "sqlite:///test.sqlite",
        "METADATA": Base.metadata
    }
}

app = App(
    routes=routes,
    settings=settings,
    commands=sqlalchemy_backend.commands,  # Install custom commands.
    components=sqlalchemy_backend.components  # Install custom components.
)


if __name__ == '__main__':
    app.main()

この例では、SQLAlchemyとSQLiteを使ってデータを登録している。app.pycreate_tables コマンドでテーブルを作成できる。

$ python app.py create_tables

tse 0.0.15リリース

tse 0.0.15をリリースした。

  • 組み込み関数 P() を追加

Python3以降では、print(...) の代わりに P(...) と書けるようにした。

  • コマンド置換

Python3以降では、`command` で コマンドとして command を実行するようにした。

例:

$ tse -b 'P(`ps`)'
  PID TTY          TIME CMD
18447 pts/1    00:00:00 bash
18487 pts/1    00:00:00 tse
18488 pts/1    00:00:00 sh
18489 pts/1    00:00:00 ps

これだけだとあまり役に立たないが、Python3.6以降では、fプリフィックスをつけて変数を展開できる。

$ ls | tse -s '\.py$' 'P(f`wc {L}`.rstrip())'
       0       0       0 __init__.py
     436    1191   14696 main.py

静的サイトジェネレータ Miyadaiku

ここ数年、www.python.jp は、 Pelican を使って構築していた。

Pelican は実績のある静的サイトジェネレータで使いやすくはあるが、基本的にはBlogサイトの構築ツールであり、あまり柔軟性や拡張性には重点を置かれていないように感じていた。www.python.jp 以外でもいくつかのサイト構築に使用したが、以下のような不満を感じていた。

  • アーティクルに Jinjaテンプレートを書きたい

    reStructuredTextやMarkdown には、定型文などを記述するため手段として、エクステンションやディレクティブなどを開発して組み込む仕組みがあるが、開発・管理はそれなりに面倒で、そう気軽には作れない。Jinjaのマクロ機能などを使って、手軽に拡張できる仕組みがほしい。

  • アーティクル全体を検索するAPIがない。このため、Blogサイトなどでよくある、サイドバーに「最近の記事ボックス」などを表示する機能の実装が難しい。

  • サイトのデザインテーマを指定して既存のデザインを共有する機能があるが、テーマとして指定できるのは一つだけしかなく、指定したテーマ以外からテンプレートやCSSJavascriptの共有手段する手段がない。

  • 目次の作成機能がない

  • 画像ファイルなどを、専用のディレクトリに保存しなければならない。アーティクルと同じディレクトリに保存したい。

などだ。

それでは、ということで独自に静的サイトジェネレータを開発した。

miyadaiku.github.io

Jinja2テンプレート

Miyadaikuでは、reStructuredTextとMarkdown用にJinja2を記述するための 拡張 を提供しており、テンプレートだけでなく、コンテンツ中でもJinja2でHTMLを生成できる。

Sample Miyadaiku article
---------------------------

This is a *plain* reST article.

.. jinja::
    {% for i in range(10) %}
      <p>{{ i }}</p>
    {% endfor %}

テーマ

作成したテンプレートやCSS、画像などの素材・コンテンツからPythonのパッケージを作成し、テーマとして再利用できる。Miyadaikuには組み込みのテーマとして

などがある。

また、別パッケージとして、次のようなテーマを提供している。

テーマは通常のPythonパッケージと同様に pipなどでインストールし、同時に複数のテーマを利用できる。また、テーマが利用しているテーマは自動的にインポートされる。

API

Jinja2テンプレートからコンテンツの表示や検索を行うAPIにアクセスできるので、独自のページ構成や目次などを自由に作成できる。

コンテンツオブジェクトはアーティクルなどのコンテンツや画像などの素材ファイルなどにアクセスするオブジェクトで、タイトルやカテゴリなどのメタデータの参照や、ページへのリンク作成などを行える。

コンテンツコレクション は、すべてのコンテンツを管理するオブジェクトで、条件を指定してコンテンツを検索できる。検索条件として、プロパティやタグなどのメタデータや、格納ディレクトリなどを指定できる。

tse 0.0.14リリース

tse 0.0.14をリリースした。

  • 組み込み関数 E() を追加

tseからコマンドを使うとき、毎度毎度 subprocess.check_output('ls', shell=True, universal_newline=True) とするのは面倒なので、E('ls') で呼び出せるようにした。

例:

$ tse -b 'for l in E("ls").splitlines():print(l)'
HISTORY
LICENSE
MANIFEST.in
README.rst

真空調理法(sous vide)の基礎調査

sous vide

  • 真空調理法または低温調理法と呼ばれる。
  • フランス語で「真空で」という意味。「スービド」って発音するのかな。
  • 肉などの食材をビニール袋に入れて空気を抜き、お湯につける調理方法。
  • 一定の温度を保ったお湯を使って、長時間加熱して調理する。
  • 空気を抜くのは、お湯と食材を直接接触させて、熱を伝えやすくするため。空気は熱伝導率が悪い。
  • 煮たり焼いたりする調理法と比べて、食材の食感や香りを損なわず、焦がしたり、茹ですぎたりすることがない。
  • ビニール袋に密閉するので、肉汁や香りを逃さない。
  • 調理中はほったらかしで良い。
  • 温度設定により、好みの焼き加減に調整できる。
  • 調理時間が長すぎても、それほど料理の完成度に影響しない。
  • おなじ温度設定で調理すれば、同じような出来栄えとなり、失敗が少ない。家庭でも、一流レストランと同じ仕上がりを期待できる。
  • 100g50円の鳥むね肉がけっこうなごちそうに化ける。

調理器具

  • 私は Anova Precision Cooker を使っている。
  • アメリカ製
  • 日本語版は販売されておらず、直輸入品しかないい。
  • 水をはった鍋に突っ込んでコンセントをつなげば、0.5℃単位で一定の温度を維持できる。
  • Bluetooth/WifiiPhoneAndroidスマホと接続し、温度や調理時間を設定できる。
  • 旧型はBluetoothのみ。下のリンクはBluetooth版。
  • 深さが15cm程度ある鍋が必要。
  • ビニール袋はジップロックを使用している。
  • ジップロックに食材を入れて、開口部以外を水に沈めて封をすれば、簡単に空気を抜いた状態にできる。

温度設定と加熱時間

  • 肉のタンパク質は50℃ぐらいから変質が始まる。
  • 65℃を越すと、タンパク質が水分を放出し始め、パサつきはじめる。
  • したがって、50℃以上、65℃未満で加熱する。
  • タンパク質のうち、コラーゲンは約65℃で収縮を始め、硬くなる。
  • コラーゲンは75~85℃で軟化をはじめる。
  • しかし、コラーゲンは低温度でも長時間加熱することでゼラチン化し、柔らかくなる。
  • コラーゲンの多い(硬い)牛肉などでも、長時間(12時間〜? 24時間〜?)加熱を続けることで、コラーゲンをゼラチン化できる。
  • 鶏肉などの柔らかい肉は、中心部まで十分に加熱されれば、それ以上加熱する必要はない。
  • フライパンで焼く場合などと違って、細かく加熱時間を気にしなくとも良い。長すぎると徐々に水分が失われていくが、それほどシビアなものではない。

低温殺菌

重要な注) 私は、食品衛生に関する専門的な知識は、一切持ち合わせていない。以下は、私が各種ウェブサイトなどを参照して得た断片的な知識を書き出したものである。単なるポエムであり、信頼できる調査結果ではない。以下の記述によってあなたがどんな被害を受けようと、当方は一切感知しない。

  • 厚生労働省の基準では、加熱食肉製品は「中心部の温度を 63°で 30 分間加熱する方法又はこれと同等以上」で殺菌しなければならない、とある。肉の厚みに応じて、63℃で 数時間加熱すれば低温殺菌として十分だろう。
  • 63℃より低温でも、大腸菌サルモネラ菌などは長時間加熱することで殺菌できる。(http://www.douglasbaldwin.com/sous-vide.html#Safety)
  • ボツリヌス菌は、低温では殺菌できない。
  • ボツリヌス菌のヤバイのは、ボツリヌス菌の毒素。ボツリヌス菌自身は、乳児以外は食べても問題はない。
  • ボツリヌス菌の毒素は、83℃5分の加熱で分解できる。
  • ボツリヌス菌の毒素は、低酸素状態でのみ発生する。通常の流通で入手した肉で、腐敗していなければ、それほど心配はいらない?
  • 真空パックされた冷凍生肉などは、加熱しないとリスクが大きい?
  • 牛肉のブロックなどでは、汚染されたとしても肉の表面のみで、肉の内部にまでは入り込む可能性は低い。したがって、低温調理の前または後にフライパンやオーブンで表面を焼く方法もある。

参考にしたURL

Python 3.6の概要 (最終回 - オプティマイズ)

Python 3.6では、長年に渡って使われてきた、Python の基本的な実装に関わる部分で、重要な変更が行われている。

辞書オブジェクトのレイアウト変更

Raymond Hettingerのアイデア を元に、稲田さん が実装したもので、古くから使われてきたPythonの辞書オブジェクトが修正され、よりメモリ効率の良い形式でデータを格納するように変更された。

この修正により、辞書オブジェクトのメモリ使用量が20〜25%程度改善されている。

バイトコードからワードコードへ

Pythonインタープリタは、ソースコードバイトコード と呼ばれる中間言語に変換し、実行する仕組みになっている。従来、バイトコード はその名の通り8bit長のデータの集まりだったが、Python3.6では、バイトコードのサイズは8bitから16bitに拡張された。

これまでのバイトコードでは 命令コード+可変長の引数 という形式の中間言語を解釈しながら実行していたが、新しい形式では、ほぼ固定長の中間言語を読み込んで実行できるようになり、Pythonインタープリタがシンプルに、効率よく動作するようになった。(https://bugs.python.org/issue26647)

PyMem_* APIが pymalloc を使用

Python C APIには、メモリの取得・開放を行うAPIPyMem*系、PyObect*系、PyMem_Raw*系の3種類がある。PyMem*系のAPIは古くからあるAPIで、C言語ランタイムライブラリの malloc()free() などをそのまま使用している。

一方、PyObject系のメモリ操作APIは、一般に pymalloc と呼ばれるPython専用のメモリアロケータを使用しており、Pythonでのメモリ利用形態に合わせて、小さくて数の多いメモリ割り当てを効率良く行えるようにしている。

pymalloc が導入された頃は、すべてのメモリ割り当てをpymallocにしてしまうとサードパーティ製の拡張モジュールなどで動作しなくなるケースが多く、完全に pymalloc に切り替えることはできなかった。そこで、PyMem*系のAPIは変更せずに malloc()を使用する実装のまま残され、新しいコードでは PyObjectAPIを使用するようになった。

しかし、pymalloc 導入からもう20年近く経過し、この間にpymallocが進化したこともあって,そろそろPyMem*APIでも pymallocを使用してもよいのではないか、ということになった。そこで3.6から PyObjectAPIと同じくpymallocを使用するように変更された。この変更により、実質的に、PyMemAPIは必要なくなっている。

既存の拡張モジュールなどでは、正しくPyMem*APIを使用していれば、この修正による不具合は発生しないはずだ。現在、C言語などによる拡張モジュールを開発している場合、正しくメモリ管理APIを利用できているか自信がなければ、デバッグビルドのPythonでテストしてみると、GILを確保しない状態でのAPI呼び出しなどの不正を検出できるので試してみると良いかもしれない。

https://bugs.python.org/issue26249