Chapter 10: サービス

サービス

Services

Web Services
API

W3Cではウェブサービスを "ネットワーク上のマシン同士の相互通信をサポートするために設計されたソフトウェアシステム" と定義しています。これは広義の意味であり、マシンと人間の通信のために設計されたプロトコルを除く、XML、JSON、RSSなどのマシン同士の通信の多くのプロトコルを含んでいます。

この章ではweb2pyを使用したサービスを、どのように公開するか解説します。もしサードパーティサービス(Twitter, Dropbox など)を活用した例に興味がある場合は、9章及び14章を参照してください。

web2pyは標準でXML、JSON、RSS、CSV、XMLRPC、JSONRPC、AMFRPC、SOAPを含む多くのプロトコルをサポートしています。また、web2pyはプロトコルを追加して拡張することもできます。

それぞれのプロトコルは複数の方法でサポートがされており、以下のように区別することにします:

  • 指定されたフォーマットで関数の出力をレンダリング(例えば、XML、JSON、RSS、CSV)
  • リモートプロシージャコール(例えば、XMLRPC、JSONRPC、AMFRPC)

辞書のレンダリング

HTML, XML, そして JSON

HTML
XML
JSON

次のコードを考えてみてください:

def count():
    session.counter = (session.counter or 0) + 1
    return dict(counter=session.counter, now=request.now)

このアクションは、訪問者がページをリロードするたびに一つずつ増加するcounter値と、現在のページリクエストのタイムスタンプを返します。

このページは、次のURLからリクエストされます:

http://127.0.0.1:8000/app/default/count

ページは、HTMLでレンダリングされます。URLに拡張子を追加することで一行のコードも書かずに、web2pyに違うプロトコルのページのレンダリングを指示することができます:

http://127.0.0.1:8000/app/default/count.html
http://127.0.0.1:8000/app/default/count.xml
http://127.0.0.1:8000/app/default/count.json

これらにより返されるレンダリング済みの辞書データは、それぞれHTML、XML、JSONになります。

XMLでのアウトプットです:

<document>
   <counter>3</counter>
   <now>2009-08-01 13:00:00</now>
</document>

JSONでのアウトプットです:

{ 'counter':3, 'now':'2009-08-01 13:00:00' }

date、time、dateimeオブジェクトが、ISOフォーマットの文字列でレンダリングされている点に注意してください。これはJSONの標準機能というよりweb2pyの仕様です。

汎用ビュー

例えば、".xml" 拡張子が呼び出された場合、web2pyは "default/count.xml" という名称のテンプレートファイルを探します。もし見つからない場合は、"generic.xml" というテンプレートを参照します。"generic.html"、"generic.xml"、"generic.json" というファイルは、ひな形アプリケーションで用意しています。それ以外の拡張子についても、ユーザーによって簡単に定義可能です。

セキュリティ上の理由から、汎用ビュー(訳注:原文は generic view、以下同様)はローカルホスト上のみにアクセスを許可しています。リモートクライアントからのアクセスを有効にするためには、response.generic_patternsを設定する必要があります。

ひな形アプリケーションのコピーを使用している場合は、models/db.py にある次の行を編集してください。

  • ローカルホストのみにアクセスを制限
response.generic_patterns = ['*'] if request.is_local else []
  • 全ての汎用ビューへのアクセスを許可
response.generic_patterns = ['*']
  • .json のみへアクセスを許可
response.generic_patterns = ['*.json']

generic_patterns は glob パターンです(訳注: 詳しくは python glob モジュールを参照)。これはアプリケーションのアクションとマッチする任意のパターンを使用するか、パターンのリストを渡すことができることを意味しています。

response.generic_patterns = ['*.json','*.xml']

古いweb2pyアプリケーションでこの機能を利用するには、(バージョン1.60以降の)ひな形アプリケーションから、"generic.*" ファイルをコピーする必要があります。

以下は "generic.html" のコードです。

{{extend 'layout.html'}}

{{=BEAUTIFY(response._vars)}}

<button onclick="document.location='{{=URL("admin","default","design",
args=request.application)}}'">admin</button>
<button onclick="jQuery('#request').slideToggle()">request</button>
<div class="hidden" id="request"><h2>request</h2>{{=BEAUTIFY(request)}}</div>
<button onclick="jQuery('#session').slideToggle()">session</button>
<div class="hidden" id="session"><h2>session</h2>{{=BEAUTIFY(session)}}</div>
<button onclick="jQuery('#response').slideToggle()">response</button>
<div class="hidden" id="response"><h2>response</h2>{{=BEAUTIFY(response)}}</div>
<script>jQuery('.hidden').hide();</script>

以下は "generic.xml" のコードです。

{{
try:
   from gluon.serializers import xml
   response.write(xml(response._vars),escape=False)
   response.headers['Content-Type']='text/xml'
except:
   raise HTTP(405,'no xml')
}}

以下は "generic.json" のコードです。

{{
try:
   from gluon.serializers import json
   response.write(json(response._vars),escape=False)
   response.headers['Content-Type']='text/json'
except:
   raise HTTP(405,'no json')
}}

pythonの基本データ型(整数、浮動小数、文字、リスト、タプル、辞書)だけで構成される限り、どのような辞書でも HTML、XML、JSONでレンダリングできます。response._vars は、アクションによって返される辞書が含まれています。

その他のユーザー定義オブジェクトやweb2py独自のオブジェクトが辞書に含まれる場合、カスタムビューを使ってレンダリングする必要があります。

Rows のレンダリング

as_list

select(訳注: DALのselect)で生成したRowsを XML、JSON、またはそれ以外のフォーマットでレンダリングするには、最初に as_list() メソッドを使用して辞書のリスト(訳注: 各レコードは辞書で、それのリスト)に変換します。

次の例を考えてみます:

db.define_table('person', Field('name'))

次のコードはHTMLにレンダリングできますが、XMLやJSONにはできません:

def everybody():
    people = db().select(db.person.ALL)
    return dict(people=people)

しかし次のコードでは、XMLとJSONのレンダリングが可能です:

def everybody():
    people = db().select(db.person.ALL).as_list()
    return dict(people=people)

カスタムフォーマット

例えば、Python pickle形式でアクションをレンダリングする場合は:

http://127.0.0.1:8000/app/default/count.pickle

単に次のコードを含んだ "default/count.pickle" という、新しいビューファイルを作成するだけです:

{{
import cPickle
response.headers['Content-Type'] = 'application/python.pickle'
response.write(cPickle.dumps(response._vars),escape=False)
}}

もし任意のアクションをPickle化ファイルとしてレンダリングしたい場合、上記のファイルを "generic.pickle" という名前で保存するだけです。

ただし、全てのオブジェクトがPickle化できるわけでは無く、また全てのPickle化したオブジェクトを元に戻せるわけではありません。Pythonの基本データ型とその組み合わせたものは、問題ありません。また、ファイルストリームやデータベース接続への参照を持たないオブジェクトも、通常はPickle化できます。しかし、事前に全てのPickle化したオブジェクトのクラスが定義されている状況下でしか、Pickle化したオブジェクトを元に戻すことができません。

RSS

RSS

web2pyにはアクションが返した辞書データをRSSフィードとしてレンダリングする、"generic.rss" ビューがあります。

RSSフィードは動作時に固定のデータ構造(タイトル、リンク、説明、アイテム、等々)を持つため、アクションによって返される辞書データは適切な構造を持つ必要があります:

{'title'      : '',
 'link'       : '',
 'description': '',
 'created_on' : '',
 'entries'    : []}

RSSフィードのentries項目内の各エントリも、同じような構造を持つ必要があります:

{'title'      : '',
 'link'       : '',
 'description': '',
 'created_on' : ''}

例えば、次のコードはRSSフィードとしてレンダリングできます:

def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=[
                  dict(title="my feed",
                  link="http://feed.example.com",
                  description="my first feed")
                ])

単に、次のURLにアクセスすることによって動作します:

http://127.0.0.1:8000/app/default/feed.rss

別の方法として、まず次のモデルを想定してみます:

db.define_table('rss_entry',
    Field('title'),
    Field('link'),
    Field('created_on','datetime'),
    Field('description'))

そして次のコードを使えば、RSSフィードとしてレンダリング可能です:

def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=db().select(db.rss_entry.ALL).as_list())

Rowsオブジェクトの as_list() メソッドは、rows(訳注: selectの結果としてのrows)を辞書のリストに変換します。

ここでは明記されていないキー名の追加の辞書項目がある場合は、無視されます。

次は、web2pyの "generic.rss" ビューのコードです:

{{
try:
   from gluon.serializers import rss
   response.write(rss(response._vars),escape=False)
   response.headers['Content-Type']='application/rss+xml'
except:
   raise HTTP(405,'no rss')
}}

もう一つのRSSアプリケーションの例として、"slashdot" フィードからデータを収集し新しいweb2py RSSフィードを返す、RSSアグリゲータ(訳注: RSSリーダ)を考えてみましょう。

def aggregator():
    import gluon.contrib.feedparser as feedparser
    d = feedparser.parse(
        "http://rss.slashdot.org/Slashdot/slashdot/to")
    return dict(title=d.channel.title,
                link = d.channel.link,
                description = d.channel.description,
                created_on = request.now,
                entries = [
                  dict(title = entry.title,
                  link = entry.link,
                  description = entry.description,
                  created_on = request.now) for entry in d.entries])

以下からアクセスできます:

http://127.0.0.1:8000/app/default/aggregator.rss

CSV

CSV

コンマ区切り(CSV)フォーマットは表形式のデータをあらわすプロトコルです。

次のモデルを考えてみます:

db.define_table('animal',
    Field('species'),
    Field('genus'),
    Field('family'))

そして次のコード:

def animals():
    animals = db().select(db.animal.ALL)
    return dict(animals=animals)

web2pyには "generic.csv" がありません。このためanimalsをCSVにシリアライズするカスタムビューとして、"default/animals.csv" を定義する必要があります。実装例を示します:

{{
import cStringIO
stream=cStringIO.StringIO()
animals.export_to_csv_file(stream)
response.headers['Content-Type']='application/vnd.ms-excel'
response.write(stream.getvalue(), escape=False)
}}

"generic.csv" を定義しておくことは可能ですが、シリアライズ化するオブジェクト名(例では "animals")を明示する必要があります。このため、"generic.csv" ファイルはweb2pyでは提供しません。

リモートプロシージャコール

RPC

web2pyはどのような関数でもウェブサービスにするメカニズムがあります。 ここでいうメカニズムとは、前述したメカニズムとは以下の場合で異なります:

  • 関数が引数を持つ場合
  • 関数がコントローラではなくモデルやモジュールで指定されている場合
  • サポートされるべきRPCメソッドを詳細に指定したい場合
  • より厳格なURL命名規則を強制する場合
  • 固定プロトコルの組み合わせで動くことで、拡張性は良くないが、以前より高い機能を実現する場合

これらの機能を使うために:

まず最初に、サービスオブジェクトをインポートしてインスタンス化します。

from gluon.tools import Service
service = Service()

これはひな形アプリケーションの "db.py" モデルファイルの中で既に行っています。

二番目に、コントローラー内でサービスハンドラを公開します:

def call():
    session.forget()
    return service()

これはひな形アプリケーションの "default.py" コントローラーで既に定義しています。サービスでセッションクッキーを使用する場合は、session.forget() を除いてください。

三番目に、サービスとして公開する関数にデコレータをつける必要があります。次のものが、現在サポートされているデコレータのリストです:

@service.run
@service.xml
@service.json
@service.rss
@service.csv
@service.xmlrpc
@service.jsonrpc
@service.jsonrpc2
@service.amfrpc3('domain')
@service.soap('FunctionName',returns={'result':type},args={'param1':type,})

例として、以下のデコレータ関数を考えてみます:

@service.run
def concat(a,b):
    return a+b

この関数はモデルもしくは、call アクションが定義されているコントローラで定義可能です。関数は次の2つの方法で、リモートから実行可能です:

http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/run/concat/hello/world

httpリクエストは、どちらの方法でも以下の値を返します:

helloworld

@service.xml デコレータを使用する場合は、次のURL経由で呼び出すことができます:

http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/xml/concat/hello/world

出力はXMLで返します:

<document>
   <result>helloworld</result>
</document>

これがDAL Rowsオブジェクトだとしても、関数の出力はシリアライズされます。このケースでは as_list() を自動で呼び出します。

@service.json デコレータを使用する場合は、次のURL経由で呼び出すことができます:

http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/json/concat/hello/world

出力はJSONで返します。

@service.csv デコレータを使用する場合にサービスハンドラは、リストのリストのように、イテレータ(訳注:反復可能な)オブジェクトのイテレータオブジェクトを返す必要があります。例を挙げます:

@service.csv
def table1(a,b):
    return [[a,b],[1,2]]

このサービスは次のどちらかのURLで呼び出し可能です:

http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world
http://127.0.0.1:8000/app/default/call/csv/table1/hello/world

そして次の値を返します:

hello,world
1,2

@service.rss デコレータは、前節で説明した "generic.rss" と同じフォーマットの返り値を期待できます。

それぞれの関数に対して、複数デコレータの設定が可能です。

今までのところ、この節で説明した内容は前節の単なる代替手段でしかありません。サービスオブジェクトが本当の力を発揮するのは、これから説明するXMLRPC、JSONRPC、AMFRPCです。

XMLRPC

XMLRPC

"default.py" コントローラで、例えば次のコードを考えてみます:

@service.xmlrpc
def add(a,b):
    return a+b

@service.xmlrpc
def div(a,b):
    return a/b

Pythonのシェルで実行します。

>>> from xmlrpclib import ServerProxy
>>> server = ServerProxy(
       'http://127.0.0.1:8000/app/default/call/xmlrpc')
>>> print server.add(3,4)
7
>>> print server.add('hello','world')
'helloworld'
>>> print server.div(12,4)
3
>>> print server.div(1,0)
ZeroDivisionError: integer division or modulo by zero

Python xmlrpclibモジュールは、クライアントにXMLRPCプロトコルを提供します。この場合web2pyは、リモートサーバーとして動作します。

クライアントはServerProxy経由でサーバーに接続し、サーバー上のデコレータ関数をリモートから実行できます。データ(a、b)はGET/POSTを使った変数を経由するのではなく、XMLRPCプロトコルにより適切にエンコードされて関数に渡されます。そうすることで、データタイプ(int、String、その他)が保持されます。戻り値についても同様です。さらに、サーバー上で発生した例外もクライアントに戻されます。

多くのプログラミング言語(C、C++、Java、C#、Ruby、Perl)にはXMLRPCライブラリがあり、これらは互いに相互運用可能です。これは異なるプログラム言語間で、相互に通信をするアプリケーションを作成する場合の最適な方法のひとつです。

XMLRPCクライアントは、web2pyのアクション内に実装することもできます。そうすることで、あるアクションが別のweb2pyアプリケーション(インストール先が同一であっても)と、XMLRPCを使用し通信することが可能になります。この場合、セッションのデッドロックに注意してください。同一アプリケーション内でXMLRPCを利用したコードを実行する場合は、実行前にセッションロックを開放する必要があります:

session.forget(response)

JSONRPC

JSONRPC

この節ではXMLRPCと同じコード例を使用します。しかしサービスはJSONRPCを代わりに使用します:

@service.jsonrpc
@service.jsonrpc2
def add(a,b):
    return a+b

def call():
    return service()

JSONRPCはXMLRPCと非常に似ています。しかしシリアル化プロトコルはXMLの代わりにJSONを使用します。

このサービスはもちろん、どの言語のどのプログラムからも呼び出し可能です。しかしここでは、Pythonを使用します。web2pyは Mariano Reingart によって作成された、"gluon/contrib/simplejsonrpc.py" モジュールを同梱しています。上記のサービス呼び出しをどのように使うか、以下、使用例を示します:

>>> from gluon.contrib.simplejsonrpc import ServerProxy
>>> URL = "http://127.0.0.1:8000/app/default/call/jsonrpc"
>>> service = ServerProxy(URL, verbose=True)
>>> print service.add(1, 2)

jsonrpc2に対しては、"http://127.0.0.1:8000/app/default/call/jsonrpc2" を使用してください。

JSONRPC と Pyjamas

JSONRPC
Pyjamas

このアプリーケーション例として、Pyjamasを使ったJSONリモートプロシージャコールの手法を説明します。PyjamasはGoogle Web Toolkit(当初はJavaで書かれていた)のPython用移植版です。PyjamasによってPythonでクライアントアプリケーションを書くことができます。PyjamasはコードをJavaScriptに変換します。web2pyはJavaScriptを配信すると共に、クライアントやユーザ操作によるトリガーから発生したリクエストを、AJAXで通信します。

ここではPyjamasをweb2py上で、どのように動作するかを説明します。web2pyとPyjamas以外のライブラリは特に必要ありません。

JSONRPCを使って排他的にサーバーと通信するPyjamasクライアント(全てJavaScript)を利用した、シンプルな "todo" アプリケーションを作成していきます。

ステップ1、"todo" アプリケーションを作成します。

ステップ2、"models/db.py" に次のコードを記述します:

db=DAL('sqlite://storage.sqlite')
db.define_table('todo', Field('task'))
service = Service()

(注意: Serviceクラスは gluon.tools モジュールにあります)

ステップ3、"controllers/default.py" に次のコードを記述します:

    def index():
    redirect(URL('todoApp'))

    @service.jsonrpc
    def getTasks():
        todos = db(db.todo).select()
        return [(todo.task,todo.id) for todo in todos]

    @service.jsonrpc
    def addTask(taskFromJson):
        db.todo.insert(task= taskFromJson)
        return getTasks()

    @service.jsonrpc
    def deleteTask (idFromJson):
        del db.todo[idFromJson]
        return getTasks()

    def call():
        session.forget()
        return service()

    def todoApp():
        return dict()

それぞれの関数の意味は明らかだと思います。

ステップ4、"views/default/todoApp.html" に次のコードを記述します:

<html>
  <head>
    <meta name="pygwt:module"
     content="{{=URL('static','output/TodoApp')}}" />
    <title>
      simple todo application
    </title>
  </head>
  <body bgcolor="white">
    <h1>
      simple todo application
    </h1>
    <i>
      type a new task to insert in db,
      click on existing task to delete it
    </i>
    <script language="javascript"
     src="{{=URL('static','output/pygwt.js')}}">
    </script>
  </body>
</html>

このビューは、まだ未作成の "static/output/todoapp" で、Pyjamasコードを実行するだけです。

ステップ5、"static/TodoApp.py" (todoAppでなくTodoAppであることに注意してください!)に、次のクライアントコードを記述します:

from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.TextBox import TextBox
import pyjamas.ui.KeyboardListener
from pyjamas.ui.ListBox import ListBox
from pyjamas.ui.HTML import HTML
from pyjamas.JSONService import JSONProxy

class TodoApp:
    def onModuleLoad(self):
        self.remote = DataService()
        panel = VerticalPanel()

        self.todoTextBox = TextBox()
        self.todoTextBox.addKeyboardListener(self)

        self.todoList = ListBox()
        self.todoList.setVisibleItemCount(7)
        self.todoList.setWidth("200px")
        self.todoList.addClickListener(self)
        self.Status = Label("")

        panel.add(Label("Add New Todo:"))
        panel.add(self.todoTextBox)
        panel.add(Label("Click to Remove:"))
        panel.add(self.todoList)
        panel.add(self.Status)
        self.remote.getTasks(self)

        RootPanel().add(panel)

    def onKeyUp(self, sender, keyCode, modifiers):
        pass

    def onKeyDown(self, sender, keyCode, modifiers):
        pass

    def onKeyPress(self, sender, keyCode, modifiers):
        """
        This function handles the onKeyPress event, and will add the
        item in the text box to the list when the user presses the
        enter key. In the future, this method will also handle the
        auto complete feature.
        """
        if keyCode == KeyboardListener.KEY_ENTER and            sender == self.todoTextBox:
            id = self.remote.addTask(sender.getText(),self)
            sender.setText("")
            if id<0:
                RootPanel().add(HTML("Server Error or Invalid Response"))

    def onClick(self, sender):
        id = self.remote.deleteTask(
                sender.getValue(sender.getSelectedIndex()),self)
        if id<0:
            RootPanel().add(
                HTML("Server Error or Invalid Response"))

    def onRemoteResponse(self, response, request_info):
        self.todoList.clear()
        for task in response:
            self.todoList.addItem(task[0])
            self.todoList.setValue(self.todoList.getItemCount()-1,
                                   task[1])

    def onRemoteError(self, code, message, request_info):
        self.Status.setText("Server Error or Invalid Response: "                             + "ERROR " + code + " - " + message)

class DataService(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "../../default/call/jsonrpc",
                           ["getTasks", "addTask","deleteTask"])

if __name__ == '__main__':
    app = TodoApp()
    app.onModuleLoad()

ステップ6、アプリケーションを実行する前にPyjamasを起動します:

cd /path/to/todo/static/
python /python/pyjamas-0.5p1/bin/pyjsbuild TodoApp.py

これによりPythonコードがJavaScriptに変換され、ブラウザで実行できるようになります。

このアプリケーションへは、次のURLよりアクセスします:

http://127.0.0.1:8000/todo/default/todoApp

この小節は、Luke Kenneth Casson Leighton(Pyjamas開発者)の助けの下、Chris Pironsが作成し、Alexei Vinidiktovが更新しました。Pyjamas 0.5p1でテストされています。このサンプルはDjangoの [blogspot1] を参考にしています。

AMFRPC

PyAMF
Adobe Flash

AMFRPCはFlashライアントがサーバーと通信するために使用するリモートプロシージャコール・プロトコルです。web2pyはAMFRPCをサポートしていますが、PyAMFライブラリ導入済みのweb2pyソース版が必要です。LinuxやWindowsのシェルから、次のコマンドでインストールできます:

easy_install pyamf

(詳細ついてはPyAMFドキュメントを参考にしてください)

この小節では読者が既に、ActionScript言語をよく理解していることを前提とします。

2つの数値を引数とし、それらの足した結果を返すシンプルなサービスを作成します。このweb2pyアプリケーション名を "pyamf_test" として、サービス addNumbers を呼び出します。

ステップ1、Adobe Flash(MX2004以降のいずれかのバージョン)を利用して、Flash FLA ファイルの新規作成からフラッシュクライアント・アプリケーションを作成します。ファイルの最初のフレームに、以下のコードを記述します:

import mx.remoting.Service;
import mx.rpc.RelayResponder;
import mx.rpc.FaultEvent;
import mx.rpc.ResultEvent;
import mx.remoting.PendingCall;

var val1 = 23;
var val2 = 86;

service = new Service(
    "http://127.0.0.1:8000/pyamf_test/default/call/amfrpc3",
    null, "mydomain", null, null);

var pc:PendingCall = service.addNumbers(val1, val2);
pc.responder = new RelayResponder(this, "onResult", "onFault");

function onResult(re:ResultEvent):Void {
    trace("Result : " + re.result);
    txt_result.text = re.result;
}

function onFault(fault:FaultEvent):Void {
    trace("Fault: " + fault.fault.faultstring);
}

stop();

このコードでFlashクライアントは、"/pyamf_test/default/gateway" ファイルにある "addNumbers" 関数に対応するサービスに、接続することが許されます。Flashのリモート処理を有効にするために、ActionScriptバージョン2 MX リモートクラス をインポートする必要があります。Adobe Flash IDEのクラスパス設定にこれらのクラスのパスを追加するか、単純に新規作成ファイルに "mx" フォルダを追加してください。

サービスコンストラクタの引数に注意してください。最初の引数はこれから作成するサービスに対応するURLです。三つ目の引数はサービスのドメイン名です。このドメイン名は "mydomain" とします。

ステップ2、"txt_result" というダイナミックテキストフィールド(訳注: Flashのdynamic text field)を作成し、ステージ上に配置します。

ステップ3、上記のFlashクライアントと通信するweb2pyゲートウェイをセットアップします。

新しいサービスとFlashクライアント用のAMFゲートウェイをホストする、pyamf_test というweb2pyアプリケーションを新規作成します。"default.py" コントローラーを編集し、次のコードが含まれるようにします。

@service.amfrpc3('mydomain')
def addNumbers(val1, val2):
    return val1 + val2

def call(): return service()

ステップ4、コンパイルし、pyamf_test.swf という名称で SWF Flashクライアントをパブリッシュ(原文:export/publish)します。さらに、新規作成した "pyamf_test" アプリケーションの "static" フォルダーに、"pyamf_test.amf"、"pyamf_test.html"、"AC_RunActiveContent.js"、"crossdomain.xml" の各ファイルを設置します。

次のURLで、クライアントのテストができます:

http://127.0.0.1:8000/pyamf_test/static/pyamf_test.html

ゲートウェイはクライアントがaddNumbersに接続した際に、バックグラウンドで呼び出されます。

AMF3の代わりにAMF0を使用している場合は、同様に次のデコレータを使用できます:

@service.amfrpc

これは次のデコレータの代わりです:

@service.amfrpc3('mydomain')

このケースでは、サービスのURLも次のように変更する必要があります:

http://127.0.0.1:8000/pyamf_test/default/call/amfrpc

SOAP

SOAP

web2pyは、Mariano Reingartが作成したSOAPクライアントとサーバーを含んでいます。XML-RPCとほとんど同じように使うことができます:

"default.py" コントローラーで、次のコードを記述した場合を考えてみます:

@service.soap('MyAdd',returns={'result':int},args={'a':int,'b':int,})
def add(a,b):
    return a+b

pythonシェルで、以下のように実行可能です:

>>> from gluon.contrib.pysimplesoap.client import SoapClient
>>> client = SoapClient(wsdl="http://localhost:8000/app/default/call/soap?WSDL")
>>> print client.MyAdd(a=1,b=2)
{'result': 3}

返り値が文字列の時に適切なエンコーディングで取得するには、u'proper utf8 text' を指定してください(訳注: utf-8を指定するため文字列の前にuを付ける)。

サービスのWSDLは、次のURLで取得可能です。

http://127.0.0.1:8000/app/default/call/soap?WSDL

公開されているメソッドのドキュメントは、次のURLで取得可能です:

http://127.0.0.1:8000/app/default/call/soap

低レベル API とその他のレシピ

simplejson

JSON
simplejson

web2pyには、Bob Ippolitoが開発したgluon.contrib.simplejsonが含まれています。このモジュールは最も標準的な、Python-JSONのエンコーダ・デコーダを提供します。

SimpleJSONは、二つの機能から構成されます。

  • gluon.contrib.simplesjson.dumps(a) は、Pythonオブジェクト a をJSONにエンコードします。
  • gluon.contrib.simplejson.loads(b) は、b のJSONデータをPtyhonオブジェクトにデコードします。

シリアライズされるオブジェクト型には、基本型、リスト、辞書があります。複合オブジェクトは、ユーザ定義クラスを除きシリアライズ可能です。

これは低レベルAPIを使った、曜日を含むPythonのリストをシリアライズする、サンプルアクション(例えば、コントローラーは "default.py" で)を示します:

def weekdays():
    names=['Sunday','Monday','Tuesday','Wednesday',
           'Thursday','Friday','Saturday']
    import gluon.contrib.simplejson
    return gluon.contrib.simplejson.dumps(names)

下は、Ajaxリクエストを上記のアクションに送信し、JSONメッセージを受信し、対応するJavaScript変数のリストに格納する、サンプルHTMLページです:

{{extend 'layout.html'}}
<script>
$.getJSON('/application/default/weekdays',
          function(data){ alert(data); });
</script>

このコードは、jQueryの $.getJSON 関数を使用しています。この関数はAjaxの呼び出し及び応答時に、ローカルのJavaScript変数 data に曜日名を格納し、その変数をコールバック関数に返します。この例ではコールバック関数がデータを受信したことを、訪問者に単純に知らせます。

PyRTF

PyRTF
RTF

ウェブサイトでよく必要となる他の機能として、Wordで読み取ることが可能なテキスト文書の作成があります。一番簡単な方法は、Rich Text Format (RTF) 文書形式を使用することです。このフォーマットはMicrosoftによって開発され、標準フォーマットになりました。

web2pyには、Simon Cusackによって開発されGrant Edwardsによって改良されたgluon.contrib.pyrtfが含まれます。このモジュールは、色付きのテキストや画像を含むRTF文書をプログラム的に作成することができます。

次の例では、二つの基本的なRTFクラス DocumentとSectionをインスタンス化し、後者を前者に追加し、後者にダミーテキストを挿入しています:

def makertf():
    import gluon.contrib.pyrtf as q
    doc=q.Document()
    section=q.Section()
    doc.Sections.append(section)
    section.append('Section Title')
    section.append('web2py is great. '*100)
    response.headers['Content-Type']='text/rtf'
    return q.dumps(doc)

Document の最後は、q.dumps(doc) によってシリアライズされます。RTF文書を返す前にヘッダーにcontent-typeを指定する必要があることに注意してください、そうしないとブラウザーはファイルをどのように処理していいか分かりません。

ブラウザの設定に依存しますが、ファイルを保存するかテキストエディタで開くかを聞かれます。

ReportLab と PDF

ReportLab
PDF

web2pyは "ReportLab"[ReportLab] という追加ライブラリで、PDFドキュメントを作成することもできます。

web2pyソース版を実行しているのであれば、ReportLabが既にインストールされています。Windowsバイナリディストリビューションの場合は、ReportLabを "wb2py/" フォルダで解凍する必要があります。Macバイナリディストリビューションの場合は、以下のフォルダで解凍することが必要です:

web2py.app/Contents/Resources/

ReportLabがインストールされ、web2pyがそれを実行できる状態であるとします。 PDFドキュメントを作成する、"get_me_a_pdf"という簡単なアクションを作成してみます。

from reportlab.platypus import *
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib import colors
from uuid import uuid4
from cgi import escape
import os

def get_me_a_pdf():
    title = "This The Doc Title"
    heading = "First Paragraph"
    text = 'bla '* 10000

    styles = getSampleStyleSheet()
    tmpfilename=os.path.join(request.folder,'private',str(uuid4()))
    doc = SimpleDocTemplate(tmpfilename)
    story = []
    story.append(Paragraph(escape(title),styles["Title"]))
    story.append(Paragraph(escape(heading),styles["Heading2"]))
    story.append(Paragraph(escape(text),styles["Normal"]))
    story.append(Spacer(1,2*inch))
    doc.build(story)
    data = open(tmpfilename,"rb").read()
    os.unlink(tmpfilename)
    response.headers['Content-Type']='application/pdf'
    return data

tmpfilename という仮のファイル名でPDFを作成し、そのファイルから生成したPDFデータを読み出し、次にファイルを削除している点に注意してください。

ReportLab APIについての詳細は、ReportLabドキュメントを参照してください。段落空白 などを利用できる、ReportLabのPlatypus APIは特にお勧めです。

Restful Webサービス

REST

RESTは "REpresentational State Transfer" の略語です。Webサービスアーキテクチャの一種であり、SOAPのようなプロトコルのことではありません。実際、RESTには標準がありません。

大まかにRESTを言い表せば、リソースの集合体によるサービスと考えることができます。各リソースはURLによって識別されます。リソースには4つのメソッドがあります。それが、POST(create)、GET(read)、PUT(update)、DELETE です。これらの頭文字を取って、CRUD(生成-読み取り-更新-削除)と略します。クライアントは、リソースを識別するURLと、リソース処理命令であるPOST/PUT/GET/DELETEといったHTTPメソッドを使用し、HTTPリクエストを組立ててリソースと通信を行います。URLは、指定したプロトコルでエンコーディングするための、例えば json といった拡張子を持っていることがあります。

次のURLに対する、POSTリクエスト例です。

http://127.0.0.1/myapp/default/api/person

これは新しい person を作成します。person は、person テーブルのレコードのことです。しかし他のタイプのリソース(例えばファイル)であってもよいです。

同様にGETリクエストです。

http://127.0.0.1/myapp/default/api/persons.json

jsonフォマットでpersonsのリスト(person データのレコード)をリクエストしています。

次のURL対する、GETリクエストです。

http://127.0.0.1/myapp/default/api/person/1.json

jsonフォーマットで、person/1 (id==1 のレコード)に関連する情報をリクエストしています。

このweb2pyのリクエストは、次の3つのパートに分割することが可能です:

  • 最初のパートは、サービスのロケーションを識別。すなわち、サービスを公開しているアクション:
http://127.0.0.1/myapp/default/api/
  • リソースの名前(personpersonsperson/1 など)
  • 拡張子で指定する通信プロトコル

常にルータ(訳注: web2pyのrouter)を使用し、URLの不要なプレフィックスの除去が可能なことに注意してください。次は簡素化した例です:

http://127.0.0.1/myapp/default/api/person/1.json

これが次のようになります:

http://127.0.0.1/api/person/1.json

これはテストの問題であり、すでに第4章で論じました。

使用例では api というアクションをコールしています。しかし、これは必ずしも必要ではありません。実際、RESTfulサービスで公開するアクションは、好きな名前を付けることや、一つだけでなく複数作成することさえ可能です。しかしこれまで通り話を進めるために、RESTfulアクションは api と呼ぶことにします。

同様に次の2つのテーブルを前提とします:

db.define_table('person',Field('name'),Field('info'))
db.define_table('pet',Field('owner',db.person),Field('name'),Field('info'))

これらのリソースを公開します。

最初に RESTfulアクションを作成します:

def api():
    return locals()

拡張子が request args (request.args はリソースの識別に使用可能)から除かれ、また個別のメソッドでハンドルできるように、コードを修正します:

@request.restful()
def api():
    def GET(*args,**vars):
        return dict()
    def POST(*args,**vars):
        return dict()
    def PUT(*args,**vars):
        return dict()
    def DELETE(*args,**vars):
        return dict()
    return locals()

次に、GET http リクエストを生成します。

http://127.0.0.1:8000/myapp/default/api/person/1.json

呼び出し、そして返り値になるのが、GET('person','1') です。GETはこのアクション内で定義されている関数(訳注:メソッド)です。 以下、注意点となります:

  • 4つ全てのメソッドを定義する必要はありません。公開したいメソッドだけ定義すればよいです。
  • メソッド関数は、名前付き引数を取ることが可能です。
  • 拡張子は request.extension に格納されます。またコンテンツタイプは自動で設定されます。

@request.restful() デコレータは、パスの拡張子が request.extension に必ず格納されるようにします。さらに、リクエストをアクションの中の一致する関数(POST, GET, PUT, DELETE)にマッピングし、request.argsrequest.vars をマッピングした関数に渡します。

個々のレコードで、POST及びGETを行うサービスを次に作成します:

@request.restful()
def api():
    response.view = 'generic.json'
    def GET(tablename,id):
        if not tablename=='person': raise HTTP(400)
        return dict(person = db.person(id))
    def POST(tablename,**fields):
        if not tablename=='person': raise HTTP(400)
        return db.person.validate_and_insert(**fields)
    return locals()

注意点:

  • GET及びPOSTは、別々の関数によって処理します。
  • 関数は、正しい引数が渡されることを予想しています(名前付きでない引数は request.args で、名前付き引数は request.vars で解析されます)。
  • 入力が正しいことをチェックし、正しくない場合は例外を発生させます。
  • GETでは db.person(id) でSelectを実行し、返り値としてレコードを返します。出力は汎用(generic)ビューが呼び出されるため、自動で JSON に変換します。
  • POSTでは validate_and_insert(..) を実行し、返り値として新しいレコードの id か、バリデーションエラーを返します。また、POST関数の **fields 変数は、呼び出し時に渡す、postパラメータのことです。

parse_as_rest (実験的試み)

ここまで説明したロジックで、 RESTful のどのタイプのWebサービス作成に関しても十分です。しかしweb2pyには、更に役立つ機能があります。

実際、web2pyはデータベーステーブルの公開と、リソースのURLへのマッピング、及びその逆の方法についての記述構文を提供しています。

parse_as_rest

この機能は、URLパターンを使用します。 パターンは、URLからデータベースクエリーにマッピングするリクエスト変数の文字列です。 4種類の核となるパターンタイプがあります:

  • 文字列定数。例えば、"friend"
  • テーブルに対応する文字列定数。例えば、"friend[person]" は、"person" テーブルを指すURL下の "friends" にマッチします。
  • 変数は条件に使用します。例えば、"{person.id}" は db.person.name=={person.id} という条件を適用します。
  • フィールド名は ":field" で表します。

核となるパターンは次のように、"/" を使って複雑なURLパターンにまとめることが可能です。

"/friend[person]/{person.id}/:field"

これにフォームのURLを与えます。

http://..../friend/1/name

personの名前を返すperson.idに対するクエリが入っています。"friend[person]" は、"friend" にマッチし、フィルタは "person" テーブルになります。"{person.id}" は、"1" にマッチし、フィルタは "person.id==1" になります。":field" は "name" にマッチします。合わせると次を返します:

db(db.person.id==1).select().first().name

複数のURLパターンを、さまざまなタイプのリクエストに対するサービスを提供できる、一つのRESTfulアクションのリストにまとめることができます。

DALはパターンリスト機能を付加する、parse_as_rest(pattern,args,vars) メソッドを持っています。request.argsrequest.vars はパターンにマッチし、レスポンスを返します(GETのみ)。

次にもっと複雑な例を示します:

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(*args,**vars):
        patterns = [
            "/friends[person]",
            "/friend/{person.name.startswith}",
            "/friend/{person.name}/:field",
            "/friend/{person.name}/pets[pet.owner]",
            "/friend/{person.name}/pet[pet.owner]/{pet.name}",
            "/friend/{person.name}/pet[pet.owner]/{pet.name}/:field"
            ]
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):
        if table_name == 'person':
            return db.person.validate_and_insert(**vars)
        elif table_name == 'pet':
            return db.pet.validate_and_insert(**vars)
        else:
            raise HTTP(400)
    return locals()

これはリスト化したパターンに一致する、以下のURLを理解します:

  • GET 全てのpersonレコード
http://.../api/friends
  • GET 名前が "t" で始まる一つのpersonレコード
http://.../api/friend/t
  • GET 名前が "Tim" で始まる最初のpersonレコードの "info" フィールドの値
http://.../api/friend/Tim/info
  • GET 上と同じpersonレコードのpetのリスト
http://.../api/friend/Tim/pets
  • GET personの名前が "Tim" で、petの名前が "Snoopy"
http://.../api/friend/Tim/pet/Snoopy
  • GET 上と条件のpetの "info" フィールドの値
http://.../api/friend/Tim/pet/Snoopy/info

このアクションでは、2つのPOSTのurlも同様に公開しています:

  • POST 新規 friend
  • POST 新規 pet

以下、"curl" ユーティリティをインストールしている場合の使用例です:

$ curl -d "name=Tim" http://127.0.0.1:8000/myapp/default/api/friend.json
{"errors": {}, "id": 1}
$ curl http://127.0.0.1:8000/myapp/default/api/friends.json
{"content": [{"info": null, "name": "Tim", "id": 1}]}
$ curl -d "name=Snoopy&owner=1" http://127.0.0.1:8000/myapp/default/api/pet.json
{"errors": {}, "id": 1}
$ curl http://127.0.0.1:8000/myapp/default/api/friend/Tim/pet/Snoopy.json
{"content": [{"info": null, "owner": 1, "name": "Snoopy", "id": 1}]}

等価かといった単純なクエリではなく、さらに複雑なURL値を使ったクエリを宣言することも可能です。例えば、次のパターンを設定した場合、

patterns = ['friends/{person.name.contains}'

次のURLは、

http://..../friends/i

次のクエリと同等になります。

db.person.name.contains('i')

同様に次のパターンを設定した場合:

patterns = ['friends/{person.name.ge}/{person.name.gt.not}'

次のURLは、

http://..../friends/aa/uu

次のクエリと同等になります。

(db.person.name>='aa')&(~(db.person.name>'uu'))

パターンでのフィールドに有効な属性は、containsstartswithlegeltgteq (equal、デフォルト)、ne (not equal) があります。この他、date 及び datetimeフィールド用の特別な属性として、 daymonthyearhourminutesecond があります。

注意点として、パターン構文は汎用にデザインしていません。全タイプのクエリをパターンに記述することはできませんが、しかし多くは可能です。構文は将来的に拡張することができます。

条件クエリを実行する、RESTful の URL を公開したい時があります。この場合、特別な引数 queriesparse_as_rest メソッドに渡すことで実現できます。queries は、辞書の (tablename,query) で指定します。query には tablename テーブルに条件をつけてアクセスするための、 DALのクエリ式を指定します。

orderというGET変数を使用することで、結果順を変えることも可能です。

http://..../api/friends?order=name|~info

order 変数によって、name がアルファベット順、かつ、info が逆ソート順になります。

GET変数の limitoffset を指定することによって、レコード数の制限も可能です。

http://..../api/friends?offset=10&limit=1000

最初の10件は飛ばして、1000件の friends(persons)を返します。limit のデフォルト値は1000です。offset のデフォルト値は0です。

極端なケースについて考えてみます。全てのテーブル(auth_ 関係のテーブルは除く)の可能な限り全てのパターンを構築したい。任意のテキストフィールド、整数フィールド、浮動小数点フィールド(範囲による)、日付(これも範囲による)で、検索できるようにしたい。また、任意のテーブルにPOSTできるようにしたい、とします:

一般的なケースでは、たくさんのパターンが必要です。しかし web2py は簡単に作成します:

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(*args,**vars):
        patterns = 'auto'
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):
        return db[table_name].validate_and_insert(**vars)
    return locals()

patterns='auto' の設定で、web2py は auth関係のテーブル以外の全てのパターンを生成します。 また、パターンを照会するパータンもあります:

http://..../api/patterns.json

これは次のように、personpetテーブルの結果を返します:

{"content": [
   "/person[person]",
   "/person/id/{person.id}",
   "/person/id/{person.id}/:field",
   "/person/id/{person.id}/pet[pet.owner]",
   "/person/id/{person.id}/pet[pet.owner]/id/{pet.id}",
   "/person/id/{person.id}/pet[pet.owner]/id/{pet.id}/:field",
   "/person/id/{person.id}/pet[pet.owner]/owner/{pet.owner}",
   "/person/id/{person.id}/pet[pet.owner]/owner/{pet.owner}/:field",
   "/person/name/pet[pet.owner]",
   "/person/name/pet[pet.owner]/id/{pet.id}",
   "/person/name/pet[pet.owner]/id/{pet.id}/:field",
   "/person/name/pet[pet.owner]/owner/{pet.owner}",
   "/person/name/pet[pet.owner]/owner/{pet.owner}/:field",
   "/person/info/pet[pet.owner]",
   "/person/info/pet[pet.owner]/id/{pet.id}",
   "/person/info/pet[pet.owner]/id/{pet.id}/:field",
   "/person/info/pet[pet.owner]/owner/{pet.owner}",
   "/person/info/pet[pet.owner]/owner/{pet.owner}/:field",
   "/pet[pet]",
   "/pet/id/{pet.id}",
   "/pet/id/{pet.id}/:field",
   "/pet/owner/{pet.owner}",
   "/pet/owner/{pet.owner}/:field"
]}

次のように、特定のテーブルのみ autoパターンを指定することも可能です:

patterns = [':auto[person]',':auto[pet]']

smart_query (実験的試み)

smart_query

RESTfulサービスに次のような、もっと柔軟で自由なクエリーを渡したい時は、

http://.../api.json?search=person.name starts with 'T' and person.name contains 'm'

次の設定で可能です。

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(search):
        try:
            rows = db.smart_query([db.person,db.pet],search).select()
            return dict(result=rows)
        except RuntimeError:
            raise HTTP(400,"Invalid search string")
    def POST(table_name,**vars):
        return db[table_name].validate_and_insert(**vars)
    return locals()

db.smart_query メソッドは、2つの引数を取ります。

  • クエリの対象のフィールドかテーブルのリスト
  • 自然言語で表現したクエリを含む文字列

これによって、該当するレコードの db.set オブジェクトを返します。

検索文字列は評価及び実行するのではなく構文解析されます。このためセキュリティ上のリスクがないことに、注意してください。

アクセス制御

APIへのアクセスはデコレータを使用して、通常と同じように制限することが可能です。例を示します。

auth.settings.allow_basic_login = True

@auth.requires_login()
@request.restful()
def api():
   def GET(s):
       return 'access granted, you said %s' % s
   return locals()

次のようにアクセスが可能です。

$ curl --user name:password http://127.0.0.1:8000/myapp/default/api/hello
access granted, you said hello

サービスとアクセス制御

Authentication

前章で、次のようなデコレータの使用について説明をしました:

@auth.requires_login()
@auth.requires_membership(...)
@auth.requires_permission(...)

(サービスとしてのデコレートされていない)通常のアクションでは、出力がHTML以外の形式でレンダリングされている場合でも、これらのデコレータを使用することができます。

サービスとして定義され、@service... デコレータでデコレートされた関数には、 @auth... デコレータは使用すべきではありません。この二つのタイプのデコレータを、混在させることはできません。もし認証を実施する場合、call アクションをデコレートする必要があります:

@auth.requires_login()
def call(): return service()

複数のサービスオブジェクトをインスタンス化し、同じような別の関数に登録して、そのいくつかを認証付きで、他を認証なしで公開可能であることに注意してください:

public_services=Service()
private_services=Service()

@public_service.jsonrpc
@private_service.jsonrpc
def f(): return 'public'

@private_service.jsonrpc
def g(): return 'private'

def public_call(): return public_service()

@auth.requires_login()
def private_call(): return private_service()

これは呼び出し元が、(前節で触れた有効なセッションクッキーやBasic認証を利用して)HTTPヘッダーに証明書を渡すことを前提にしています。クライアントはそれをサポートしている必要があります。しかし、一部のクライアントはサポートしていません。

第3版 - 翻訳: Omi Chiba レビュー: Yota Ichino
第4版 - 翻訳: Hitoshi Kato レビュー: Mitsuhiro Tsuda
第5版 - 翻訳: Mitsuhiro Tsuda レビュー: Hitoshi Kato
 top