Discreet Blog 13.7.2006

Daily Link 2006-07-13

XML-RPC 対応 Wiki サーバ

おまけで XML-RPC。

wiki.py をそのままインポートしてます。使いまわし。

何も考えずに同名のメソッドを呼び出してるだけですが、ちゃんと動くんだから仕方ないでしょ。

# A rewritten version of xmlprc_server.py
# from "Twisted Network Programming Essentials" Example 5-3.
http://www.oreilly.com/catalog/twistedadn/
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 未対応。

posted at 21:45:52    #

REST アーキテクチャの Wiki サーバ

もうすぐさっぽろ夏まつり すげえだろ。難しそうだろ。偉そうだろ。分散ハイパーメディアシステムのためのソフトウェアアーキテクチャなんだぜ。しかも Wiki だぜ。無敵だぜ。

どのへんが REST かってえと、http://hostname:8082/WikiNameに GET でアクセスするとコンテンツの取得、PUT はコンテンツの作成(更新)、DELETE で削除するんだ。以上だ。そんだけだ。どんなもんだい。

ワールドワイドな用途を考慮して WikiName には半角アルファベットしか使えないぜ。データオブジェクトのパーシステンスには業界注目の最先端ソリューションぴっくるを採用!

# A rewritten version of wiki.py
# from "Twisted Network Programming Essentials" Example 5-1.
http://www.oreilly.com/catalog/twistedadn/

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 送受信するよう修正してあります。

posted at 19:03:44    #

この新しいブルースを楽しむような気持で

むー。

posted at 12:29:04    #
7月 2006
      1 2
3 4 5 6 7 8 9
10111213141516
17181920212223
24252627282930
31      
6月
2006
 8月
2006

Yasushi Iwata のウェブログです。

XML-Image Letterimage

© 2006-2010, Yasushi Iwata