KZKY memo

自分用メモ.

CherryPy 入門/まとめ

概要

CherryPyは,軽量なpython WAF.もっと軽量でSinatra-likeに書けるpython WAFにFlask/Bottleがあるが,オブジェクト志向のように書けて,URL Routingとオブジェクトツリーを対応させたい場合にはCherryPyが良いと思う.
Fullstack WAFだとDjangoが圧倒的だと思うが,プロト用途でWebAPIのみ作成するなら個人的にCherryPyがベスト.
ちなみに,google trendで"python flask" vs "python bottle" vs "python cherrypy"だと2011年初頭からFlask > Bottle > CherryPyのよう.

下記まとめ.

Installation

  • ubuntu12.04
$ sudo apt-get install python-cherrypy3

Hello CherryPy

  • example
#!/usr/bin/env python

import random
import string

import cherrypy
import json

class SharedData(object):
    """
    """
    data = {}
    data[10] = 10
    data[100] = 100
    data[1000] = 1000
    
    
    def __init__(self, ):
        """
        """
                
        pass

class StringGenerator(object):
    @cherrypy.expose
    @cherrypy.tools.json_out() # return val as json
    def index(self):
        return SharedData.data
        #return str(SharedData.data)
        #return "Hello world!"

    @cherrypy.expose
    def generate(self, length=8):
        return ''.join(random.sample(string.hexdigits, int(length)))

if __name__ == '__main__':
    cherrypy.quickstart(StringGenerator())

Session

  • cherrypy.session["key"] = valueを使う
  • server
import random
import string

import cherrypy

class StringGenerator(object):
    @cherrypy.expose
    def index(self):
        return """<html>
         <head></head>
     <body>
       <form method="get" action="generate">
         <input type="text" value="8" name="length" />
             <button type="submit">Give it now!</button>
       </form>
     </body>
   </html>"""

    @cherrypy.expose
    def generate(self, length=8): # mapped to action
        some_string = ''.join(random.sample(string.hexdigits, int(length)))
        cherrypy.session['mystring'] = some_string
        return some_string

    @cherrypy.expose
    def display(self):
        return cherrypy.session['mystring']

if __name__ == '__main__':
    conf = {
        '/': {
            'tools.sessions.on': True
             }
    }
    cherrypy.quickstart(StringGenerator(), '/', conf)
  • client
$ curl -X GET "http://localhost:8080/"

Dispatcher

  • urlpath, http method, query-stringによって呼び出すメソッドを変更する
  • Dispatcher を継承したクラスを作る
  • 登録
conf = {
        '/': {
            'request.dispatch': ForceLowerDispatcher(),
        }
}

cherrypy.dispatch.MethodDispatcher()

  • HTTP Methodによって呼び出す関数を変える

Extension from Dispatcher Class

  • server
import random
import string

import cherrypy
from cherrypy._cpdispatch import Dispatcher

class StringGenerator(object):
    @cherrypy.expose
    def generate(self, length=8):
        return ''.join(random.sample(string.hexdigits, int(length)))

class ForceLowerDispatcher(Dispatcher):
    def __call__(self, path_info):
        return Dispatcher.__call__(self, path_info.lower())

if __name__ == '__main__':
    conf = {
        '/': {
            'request.dispatch': ForceLowerDispatcher(),
        }
    }
    cherrypy.quickstart(StringGenerator(), '/', conf)
  • client
$ curl -X GET "http://localhost:8080/generate?length=8"
$ curl -X GET "http://localhost:8080/GENerAte?length=8"

Restful Dispatching (URL Router)

_cp_dispatch
  • server
import cherrypy

class Band(object):
    def __init__(self):
        self.albums = Album()

    def _cp_dispatch(self, vpath):
        if len(vpath) == 1:
            cherrypy.request.params['name'] = vpath.pop()
            return self

        if len(vpath) == 3:
            cherrypy.request.params['artist'] = vpath.pop(0)  # /band name/
            vpath.pop(0) # /albums/
            cherrypy.request.params['title'] = vpath.pop(0) # /album title/
            return self.albums

        return vpath

    @cherrypy.expose
    def index(self, name):
        return 'About %s...' % name

class Album(object):
    @cherrypy.expose
    def index(self, artist, title):
        return 'About %s by %s...' % (title, artist)

if __name__ == '__main__':
    cherrypy.quickstart(Band())
  • client
$ curl -X GET "http://localhost:8080/nirvana/"
$ curl -X GET "http://localhost:8080/nirvana/albums/nevermind/"
The popargs decorator
  • vpath.popの代わり
  • 基本的設計
    • noun1=class1, noun2=class2
    • class1で,@cherrypy.popargs('noun1')
    • class1が保持しているクラス=class2で,@cherrypy.popargs('noun1', 'noun2')
  • server
import cherrypy

@cherrypy.popargs('name')
class Band(object):
    def __init__(self):
        self.album = Album()

    @cherrypy.expose
    def index(self, name):
        return 'About %s...' % name

    @cherrypy.expose
    def albums(self, name):
        """
        """
        albums = ["a", "b", "c"]
        return 'Albums about %s are %s' % (name, str(albums))
        
        pass

@cherrypy.popargs('title')
class Album(object):
    def __init__(self):
        self.track = Track()
        pass

    @cherrypy.expose
    def index(self, name, title):
        return 'About %s by %s...' % (title, name)
    pass

    @cherrypy.expose
    def traks(self, name, title):
        tracks = ["a", "b", "c"]
        return 'Traks about %s by %s are %s' % (title, name, str(tracks))
    pass


@cherrypy.popargs('num', 'track')
class Track(object):

    @cherrypy.expose
    def index(self, name, title, num, track):
        return 'About %s by %s at num:%s,track:%s' % (title, name, num, track)

    @cherrypy.expose
    def details(self, name, title, num, track, **params):
        """
        e.g.,
        $ curl -X GET
        "http://localhost:8080/nirvana/album/nevermind/track/06/polly/details/"
        """

        arg1 = params["arg1"]
        arg2 = params["arg2"]
        print arg1, arg2
        return 'Detail About %s by %s at num:%s,track:%s' % (title, name, num, track)
        
        pass
    
if __name__ == '__main__':
    cherrypy.quickstart(Band())
  • client
$ curl -X GET "http://localhost:8080/nirvana/album/nevermind/track/06/polly/details?arg1=1&arg2=2"

Multiple Applications

single application

  • easiest way
cherrypy.quickstart(Blog())
cherrypy.quickstart(Blog(), '/blog')
cherrypy.quickstart(Blog(), '/blog', {'/': {'tools.gzip.on': True}})

multiple applications

  • usual way
cherrypy.tree.mount(Blog(), '/blog', blog_conf)
cherrypy.tree.mount(Forum(), '/forum', forum_conf)

cherrypy.engine.start()
cherrypy.engine.block()

RestAPI設計方針

  • ここで言うパラメータとパラメータ値はapipath構成要素のこと
  • apipath構成要素のパラメータはクラス
  • apipath構成要素のパラメータ値はcherrypy.popargs(param_val)
  • apipath構成要素のパラメータをクラスにもたせるときは,パラメータの名前と同じメンバ変数とする
  • object treeの子でpopargsする時に,親でpopargsした変数をpopargsすること.
  • get parameterは**kwargs(マップなら何でもOK)で受け取る
  • GETならこんな感じ,/noun1/val1/noun2/val1/noun3/val3?details=k1=v1&k2=v2
  • url root pathとroot classを対応させる
  • 上記popargsの例がほぼこれ.
  • nounsは別にメソッドを作成する
  • google play storeを参考にしてもいいかも

Tips

Trailing Slash

  • /path/to/?を/path/to?にしたい場合
  • /path/to/を/path/toにしたい場合
if __name__ == '__main__':
    conf = {
        "/": {
            'tools.trailing_slash.missing': False
            }
        }
    cherrypy.quickstart(ControllerClass(), "/", conf)

Class Init

  • class initは,一度だけ実行される(クラスはシングルトン的な使われ方をされる)

Autoreloader

CORS (cross origin resource sharing)

def CORS():
    cherrypy.response.headers["Access-Control-Allow-Origin"] = "*" # mean: CORS to

def main():
	# before starting
    cherrypy.tools.CORS = cherrypy.Tool("before_finalize", CORS)
    conf = {
        "/": {
            'tools.trailing_slash.missing': False,
            'tools.CORS.on': True
        }
    }

	cherrypy.quickstart(...)

In-Memoryでデータをもたせる

  • グローバル変数で持たせればいい.
  • どこかのクラスでロードすれば良い
    • クラスのinitは一度だけなので,__init__で読み込んでクラスメンバに持たせる