|
おまけで XML-RPC。
wiki.py をそのままインポートしてます。使いまわし。
何も考えずに同名のメソッドを呼び出してるだけですが、ちゃんと動くんだから仕方ないでしょ。
import wiki
from twisted.web2 import server, xmlrpc
from twisted.web2.channel import HTTPFactory
class WikiXmlRpc(xmlrpc.XMLRPC):
def __init__(self, wikiData):
super(WikiXmlRpc, self).__init__()
self.wikiData = wikiData
def xmlrpc_hasPage(self, request, wikiName):
return self.wikiData.hasPage(wikiName)
def xmlrpc_listPages(self, request):
return self.wikiData.listPages()
def xmlrpc_getPage(self, request, wikiName):
return self.wikiData.getPage(wikiName)
def xmlrpc_getRenderedPage(self, request, wikiName):
return self.wikiData.getRenderedPage(wikiName)
def xmlrpc_setPage(self, request, wikiName, data):
self.wikiData.setPage(wikiName, data)
return wikiName
if __name__ == "__main__":
import sys
from twisted.internet import reactor
wikiData = wiki.WikiData(sys.argv[1])
root = wiki.RootResource(wikiData)
root.putChild('index', wiki.IndexPage(wikiData))
root.putChild('save', wiki.SavePage(wikiData))
root.putChild('edit', wiki.EditFactory(wikiData))
root.putChild('RPC2', WikiXmlRpc(wikiData))
site = server.Site(root)
reactor.listenTCP(8082, HTTPFactory(site))
reactor.run()
twisted.web2.xmlrpc.XMLRPC の使い方は、前とさほど変ってません。
xmlrpc_ で始まる名前を持つメソッドの最初のパラメータとして Request オブジェクトが渡されるようになったことくらい。
テスト用の XML-RPC クライアントはこれ。
Twisted 本ではこの後 SOAP の例もあるんだけど、Web2 は SOAP 未対応。
|
|
すげえだろ。難しそうだろ。偉そうだろ。分散ハイパーメディアシステムのためのソフトウェアアーキテクチャなんだぜ。しかも Wiki だぜ。無敵だぜ。
どのへんが REST かってえと、http://hostname:8082/WikiNameに GET でアクセスするとコンテンツの取得、PUT はコンテンツの作成(更新)、DELETE で削除するんだ。以上だ。そんだけだ。どんなもんだい。
ワールドワイドな用途を考慮して WikiName には半角アルファベットしか使えないぜ。データオブジェクトのパーシステンスには業界注目の最先端ソリューションぴっくるを採用!
from twisted.web2 import http, http_headers, responsecode, resource, \
server, stream
from twisted.web2.channel import HTTPFactory
from twisted.web2.error import ERROR_MESSAGES
import errno
import re, pickle, os
import tempfile
ENCODING = "utf-8"
CTYPE_HTML ={
'content-type': http_headers.MimeType('text', 'html',
{'charset': '%s' % ENCODING})}
reWikiWord = re.compile(r"\b(([A-Z][a-z]+){2,})\b")
class FoundResponse(http.StatusResponse):
def __init__(self, location):
super(FoundResponse, self).__init__(
responsecode.FOUND,
ERROR_MESSAGES[responsecode.FOUND] % {"location": location})
self.headers.setHeader("location", location)
class IndexPage(resource.Resource):
def __init__(self, wikidata):
super(IndexPage, self).__init__()
self.wikiData = wikidata
def render(self, request):
pages = self.wikiData.listPages()
pages.sort()
links = ["<li><a href='%s'>%s</a></li>" % (p, p) for p in pages]
html = """
<html>
<head><title>Wiki Index</title><head>
<a href='/'>Home</a>
<h1>Index</h1>
<body>
<ul>
%s
</ul>
</body>
</html>
""" % "".join(links)
return http.Response(responsecode.OK, CTYPE_HTML, html)
class EditPage(resource.Resource):
def __init__(self, wikidata, wikiname):
super(EditPage, self).__init__()
self.wikiData = wikidata
self.wikiname = wikiname
def render(self, reuquest):
html = """
<html>
<head><title>Editing: %(wikiname)s</title></head>
<body>
Editing: %(wikiname)s
<form method='post' action='/save'>
<input type='hidden' name='wikiname' value='%(wikiname)s'>
<textarea name='data' rows='30' cols='80'>%(data)s</textarea>
<br />
<input type='submit' value='Save'>
</form>
</body>
</html>
""" % {'wikiname': self.wikiname,
'data': self.wikiData.getPage(self.wikiname)}
return http.Response(responsecode.OK, CTYPE_HTML, html)
class SavePage(resource.PostableResource):
def __init__(self, wikidata):
super(SavePage, self).__init__()
self.wikiData = wikidata
def render(self, request):
method = getattr(self, '_render%s' % request.method)
return method(request)
def _renderPOST(self, request):
data = request.args['data'][0]
wikiname = request.args['wikiname'][0]
self.wikiData.setPage(wikiname, data)
return FoundResponse('/' + wikiname)
def _renderGET(self, request):
html = """
To add a page to the Wiki, send data to this page using
HTTP POST. Arguments:
<ul>
<li><code>wikiname</code>: the name of a Wiki page</li>
<li><code>data</code>: the content to use for that page</li>
</ul>
"""
return http.Response(responsecode.OK, CTYPE_HTML, html)
class EditFactory(resource.Resource):
addSlash = True
def __init__(self, wikidata):
super(EditFactory, self).__init__()
self.wikiData = wikidata
def locateChild(self, request, segments):
if segments[0]:
return EditPage(self.wikiData, segments[0]), segments[1:]
else:
return self, segments[1:]
def render(self, request):
return http.RedirectResponse('/')
class WikiPage(resource.Resource):
def __init__(self, wikidata, wikiname):
super(WikiPage, self).__init__()
self.wikiData = wikidata
self.wikiname = wikiname
def http_PUT(self, request):
self.body = ""
def handleChunk(chunk):
self.body += chunk
def done(result):
self.wikiData.setPage(self.wikiname, self.body)
if self.wikiData.hasPage(self.wikiname):
return http.StatusResponse(
responsecode.OK, "Modified existing page")
else:
return http.StatusResponse(
responsecode.CREATED, "Created new page")
d = stream.readStream(request.stream, handleChunk)
d.addCallback(done)
return d
def http_DELETE(self, request):
if self.wikiData.hasPage(self.wikiname):
self.wikiData.deletePage(self.wikiname)
return http.StatusResponse(responsecode.NO_CONTENT, "Deleted")
else:
return http.StatusResponse(
responsecode.NOT_FOUND,
ERROR_MESSAGES[responsecode.NOT_FOUND] % {"uri": request.uri})
def render(self, request):
if self.wikiData.hasPage(self.wikiname):
html = """
<html>
<head><title>%s</title></head>
<body>
<p>
<a href='/edit/%s'>Edit Page</a>
<a href='/index'>Index</a>
<a href='/'>Home</a>
</p>
%s
</body>
</html>
""" % (self.wikiname, self.wikiname,
self.wikiData.getRenderedPage(self.wikiname))
return http.Response(responsecode.OK, CTYPE_HTML, html)
else:
return FoundResponse('/edit/%s' % self.wikiname)
class RootResource(resource.Resource):
addSlash = True
def __init__(self, wikidata):
super(RootResource, self).__init__()
self.wikiData = wikidata
def locateChild(self, request, segments):
if segments[0]:
res = getattr(self, 'child_%s' % segments[0], None)
if res:
return res, segments[1:]
else:
return WikiPage(self.wikiData, segments[0]), segments[1:]
else:
return self, segments[1:]
def render(self, request):
return WikiPage(self.wikiData, 'WikiHome')
class WikiData(object):
def __init__(self, filename):
self.filename = filename
self.pages = {'WikiHome': 'This is your Wiki home page.'}
try:
f = file(filename, 'rb')
except IOError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
else:
self.pages = pickle.load(f)
f.close()
def save(self):
tmpfd, tmp = tempfile.mkstemp()
f = os.fdopen(tmpfd, 'wb')
pickle.dump(self.pages, f)
os.fsync(f)
f.close()
os.rename(tmp, self.filename)
def hasPage(self, wikiname):
return wikiname in self.pages
def listPages(self):
return self.pages.keys()
def getPage(self, wikiname):
return self.pages.get(wikiname, '')
def getRenderedPage(self, wikiname):
pageData = self.getPage(wikiname)
s, count = reWikiWord.subn(r"<a href='\1'>\1</a>", pageData)
return s
def setPage(self, wikiname, content):
self.pages[wikiname] = content
self.save()
def deletePage(self, wikiname):
del self.pages[wikiname]
if __name__ == "__main__":
import sys
from twisted.internet import reactor
wiki = WikiData(sys.argv[1])
root = RootResource(wiki)
root.putChild('index', IndexPage(wiki))
root.putChild('save', SavePage(wiki))
root.putChild('edit', EditFactory(wiki))
site = server.Site(root)
reactor.listenTCP(8082, HTTPFactory(site))
reactor.run()
Wiki のデータ WikiName をキーにした辞書で、各階層のリソースが共有しています。
普通の Web インターフェースもサポートしていて、
http://hostname/WikiName に該当するページが存在していたらその内容を表示、なければ http://hostname/edit/WikiName リダイレクトして追加できるようになっています。
REST の処理してるのは WikiPage クラスです。http_PUT() と http_DELETE()
が追加されてます。この2つは render() を呼び出さずに直接 Response オブジェクトを返しています。GET のときだけ render() が実行されます。
request.stream というのが http_PUT で初登場。リクエストのボディデータを処理しています。関数 twisted.web2.stream.readStream() を使用。readStream() のパラメータにコールバックとして handleChunk() を指定しています。この関数はデータを受信する都度呼び出されます。データの受信が完了したら、done() が実行されるわけです。
でっかいデータが飛んでくるときは、届いたものから順次一時ファイルに書き出すなどの処理をしなければなりません。その場合の処理関数としては twisted.web2.stream に readIntoFile() というのがあります。
テスト用の REST クライアントは これ 。Web2 のクライアント・モジュールはまだないので、twisted.web のやつです。
元のコードでは DELETE 成功時のレスポンスコードが 200 OK になっていたのですが、サーバ、クライアント共に 204 No Content 送受信するよう修正してあります。
|