2008/04/24


Google App Engine上でweb.pyとかCherryPyとか動かないという報告がいくらかあったのでまとめてみます。
Google App Engineでは、以下で述べられているように「WSGIに対応したCGI(と呼ばれるもの)であれば、フレームワークとして扱えるよ」と言っています。
Using the webapp Framework - Google App Engine - Google Code

The CGI standard is simple, but it would be cumbersome to write all of the code that uses it by hand. Web application frameworks handle these details for you, so you can focus your development efforts on your application's features. Google App Engine supports any framework written in pure Python that speaks CGI (and any WSGI-compliant framework using a CGI adaptor), including Django, CherryPy, Pylons, and web.py. You can bundle a framework of your choosing with your application code by copying its code into your application directory.

http://code.google.com/appengine/docs/gettingstarted/usingwebapp.html
今日はこの動かないと言われている部分を解決して頂ける(かもしれない)ポイントをご紹介。

web.py

まずweb.pyですが、オフィシャルがtarballで配布している物(web.py-0.23.tar.gz)では動きません。最新はhttp://webpy.org/bzr/webpy.dev/で配布されており、bzrを使って取得する必要があります(必要であればhttp://bazaar-vcs.org/Downloadからbzrを取得して下さい)。 bzr get http://webpy.org/bzr/webpy.dev/
これまでの1スクリプトからソケットサーバまで起動するweb.pyのコーディング習慣ではそのままでは動きません。flupを使ってWSGIモジュールを動かす必要があります。以下の様にWSGIハンドラとしてアプリケーションを作成しwsgiref.handlersに起動させます。mod_pythonの場合と同じですかね。
ソースコードの中を覗いた所、Google App Engineに対応するコードが入っています。以前までは一般的なCGIとして「print "Hello World!"」してしまえばそのまま出力されていましたが方式が変った様で、GET等はreturnで文字列として返す様になっています。
動くコードとしては以下の様になります。
#!-*- coding:utf-8 -*-
import web

urls = (
  '/hello/(.*)', 'hello'
)

class hello:
  def GET(self, name):
    web.header("Content-Type", "text/html; charset=utf-8")
    return "Hello World!"

if __name__ == "__main__":
  web.application(urls, globals()).cgirun()
但し、「web.pyが内部でimportしているopenid.consumerが無いよ!」と怒られるのでpython-openidをいっそ入れてしまうか、以下のパッチを当てる必要があります。 --- web/__init__.py.orig    Wed Apr 23 19:12:33 2008
+++ web/__init__.py Wed Apr 23 19:12:35 2008
@@ -26,7 +26,7 @@
 from httpserver import *
 from debugerror import *
 from application import *
-import webopenid as openid
+#import webopenid as openid
 
 try:
     import cheetah
まだ開発版の様ですから、今後に期待したいです。

CherryPy

次にCherryPyですが少し小細工が必要です。Google App EngineではPure Pythonで無いものは動かないのですがCherryPyに含まれるWSGIServer(実際にはSSL機能)がsocket._fileobjectを使ってしまっていてモジュールのインポートに失敗します。以下の様にしてSSL_fileobjectを殺してやる必要があります。
--- cherrypy/wsgiserver/__init__.py.orig    Sun Jan 13 17:56:50 2008
+++ cherrypy/wsgiserver/__init__.py Wed Apr 23 16:47:34 2008
@@ -57,11 +57,12 @@
 from urllib import unquote
 from urlparse import urlparse
 
-try:
-    from OpenSSL import SSL
-    from OpenSSL import crypto
-except ImportError:
-    SSL = None
+#try:
+#    from OpenSSL import SSL
+#    from OpenSSL import crypto
+#except ImportError:
+#    SSL = None
+SSL = None
 
 import errno
 socket_errors_to_ignore = []
@@ -676,19 +677,19 @@
                 raise socket.timeout("timed out")
     return ssl_method_wrapper
 
-class SSL_fileobject(socket._fileobject):
-    """Faux file object attached to a socket object."""
-    
-    ssl_timeout = 3
-    ssl_retry = .01
-    
-    close = _ssl_wrap_method(socket._fileobject.close)
-    flush = _ssl_wrap_method(socket._fileobject.flush)
-    write = _ssl_wrap_method(socket._fileobject.write)
-    writelines = _ssl_wrap_method(socket._fileobject.writelines)
-    read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
-    readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
-    readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
+#class SSL_fileobject(socket._fileobject):
+#    """Faux file object attached to a socket object."""
+#    
+#    ssl_timeout = 3
+#    ssl_retry = .01
+#    
+#    close = _ssl_wrap_method(socket._fileobject.close)
+#    flush = _ssl_wrap_method(socket._fileobject.flush)
+#    write = _ssl_wrap_method(socket._fileobject.write)
+#    writelines = _ssl_wrap_method(socket._fileobject.writelines)
+#    read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
+#    readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
+#    readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
 
 
 class HTTPConnection(object):
あとはweb.py同様に import cherrypy
import wsgiref.handlers

class OnePage(object):
  def index(self):
      return "one page!"
  index.exposed = True
 
class HelloWorld(object):
  onepage = OnePage()

  def index(self):
    return "hello world"
  index.exposed = True

def main():
  app = cherrypy.Application(HelloWorld(), "/helloworld")
  wsgiref.handlers.CGIHandler().run(app)

if __name__ == '__main__':
  main()
とすればGoogle App Engine上でも動きます。上の例にあるCherryPy独特の「/helloworld/onepage」も動きますよ!
これで幾らかのフレームワークが動くようになりました。幾らか敷居が低くなるのではないでしょうか。皆さんも色んなアプリケーションを作ってみませんか。
また時間が出来たら、残るPylonsも検証して見たいと思います。

最後に私が好きなCDを...
いけないチェリー・パイ いけないチェリー・パイ
ウォレント
Sony Music Direct CD / ¥1,575 (2004年07月22日)
 
発送可能時間:

Posted at by




こんなのあるんだ...
freshmeat.net: Project details for pytumblr - ロックスターになりたい
pytumblr is a Python library for the tumblr.com API. freshmeat.net: Project details for pytumblr
Google App Engine そう言えば前に「Windowsのエクスプローラで「送る」からShareOnTumblr」なんてのも作ったなぁ。 pythonで作ってあってlinuxなんかでも動くように作ったはず。

でpytumblrですが、ソース見たら簡単なソースだったのでGoogle App Engineで動くように改造してみました。以下パッチ
--- pytumblr.py.orig    Thu Apr 24 03:15:26 2008
+++ pytumblr.py Thu Apr 24 12:02:32 2008
@@ -1,7 +1,9 @@
 #!/usr/bin/env python
+#!-*- coding:utf-8 -*-
 
-import string, httplib, urllib2, urllib
-from xml.dom import minidom
+import urllib2, urllib
+from google.appengine.api import urlfetch
+from BeautifulSoup import BeautifulSoup
 
 class pytumblr(object):
    """Tumblr API Object.
@@ -75,14 +77,11 @@
 
        data = urllib.urlencode(values)
        headers = {"Content-type": "application/x-www-form-urlencoded"}
-       conn = httplib.HTTPConnection(self.url)
-       conn.follow_all_redirects = True
-       conn.request("POST",'/api/write', data, headers)
-       response = conn.getresponse()
-       if ( int(response.status) == 201):
+       response = urlfetch.fetch("http://%s/api/write" % self.url, headers=headers, method='POST', payload=data)
+       if ( int(response.status_code) == 201):
            return 'Success'
-       elif( int(response.status) != 201):
-           raise 'Error - Status %s (%s) returned' %(response.status, response.reason)
+       elif( int(response.status_code) != 201):
+           raise 'Error - Status %s (%s) returned' %(response.status_code, 'something wrong')
 
    def auth(self):
        values = {
@@ -92,13 +91,10 @@
            }
        data = urllib.urlencode(values)
        headers = {"Content-type": "application/x-www-form-urlencoded"}
-       conn = httplib.HTTPConnection(self.url)
-       conn.follow_all_redirects = True
-       conn.request("POST", '/api/write', data, headers)
-       response = conn.getresponse()
-       if ( int(response.status) != 200 ):
-           return '< There was a tiny problem: %s (%s) >' %(response.status, response.reason)
-       if ( int(response.status) == 200 ):
+       response = urlfetch.fetch("http://%s/api/write" % self.url, headers=headers, method='POST', payload=data)
+       if ( int(response.status_code) != 200 ):
+           return '< There was a tiny problem: %s (%s) >' %(response.status_code, 'something wrong')
+       if ( int(response.status_code) == 200 ):
            return '< Authenticated! >'
    
 
@@ -112,25 +108,8 @@
            opts = opts + "?type=" + type
        if ( id != 'None' ):
            opts = opts + "?id=" + id
-       return urllib.urlopen('http://%s.tumblr.com/api/read%s' %(self.user, opts)).read()
+       return urlfetch.fetch('http://%s.tumblr.com/api/read%s' %(self.user, opts)).content
        
    def getblog(self):
-       rxml = minidom.parseString(self.blogread())
-       titles = rxml.getElementsByTagName('regular-title')
-       postid = rxml.getElementsByTagName('post')
-       i = 0
-       n = 0
-       posts = {}
-       while ( i < len(postid)):
-           if ( postid[i].attributes['type'].value == 'regular' ):
-               t = titles [n]
-               t = t.toxml()
-               t = t.replace('<regular-title)', '')
-               t = t.replace('</regular-title)', '')
-               poid = postid[i].attributes["id"].value
-               posts[ poid ] = t
-               n = n + 1
-           if ( postid[i].attributes['type'].value != 'regular' ):
-               pass
-           i = i + 1
-       return posts
+       soap = BeautifulSoup(self.blogread())
+       return soap('post')
思いっきり弄ってますね。。。
minidomの代わりにBeautifulSoupを、urllib2の代わりにurlfetchを使っています。したがってgetblogは自分のregular情報のdictだけを返すのではなくBeautifulSoupを使ってlink、photo、quoteを返す様にしてあります。
実際に動くよって所は以下のサイトで確認して下さい。
pytumblr
このサイトのURLの後ろに http://mattn.appspot.com/tumblr/mattn と言った感じにtumblrアカウント名を付けてみて下さい。
今回のデモにはweb.pyというフレームワークを使用してみました。
以下スクリプトソースです。 #!-*- coding:utf-8 -*-
import web
from pytumblr import pytumblr
from BeautifulSoup import BeautifulSoup

def unescape(str):
  return  BeautifulSoup(str, convertEntities=BeautifulSoup.HTML_ENTITIES).contents[0].encode('utf-8', 'replace')

urls = (
  '/tumblr/(.*)', 'tumblr'
)
render = web.template.render('templates/')

class tumblr:
  def GET(self, name):
    web.header("Content-Type", "text/html; charset=utf-8")
    template_values = { 'name': '', 'posts': [] }
    name = name.replace('/', '')
    if name:
      pt = pytumblr(name, None, None)
      template_values['name'] = name
      for blog in pt.getblog():
        if blog['type'] == 'link':
          template_values['posts'].append({
            'type': 'link',
            'text': unescape(blog('link-text')[0].string),
            'desc': blog('link-description') and unescape(blog('link-description')[0].string) or '',
            'link': unescape(blog('link-url')[0].string),
          })
        if blog['type'] == 'photo':
          template_values['posts'].append({
            'type': 'photo',
            'text': unescape(blog('photo-caption')[0].string),
            'link': unescape(blog('photo-url')[0].string),
          })
        if blog['type'] == 'quote':
          template_values['posts'].append({
            'type': 'quote',
            'text': unescape(blog('quote-text')[0].string),
            'link': unescape(blog('quote-source')[0].string),
          })
    return render.tumblr(template_values)

if __name__ == "__main__":
  web.application(urls, globals()).cgirun()
案外短く書けますね。そしてテンプレートHTML
$def with (res)
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/static/css/tumblr.css" type="text/css" />
$if res['name']:
    <title>pytumblr - $res['name']</title>
$else:
    <title>pytumblr</title>
</head>
<body>
$if res['name']:
    <h1>pytumblr - $res['name']</h1>
$else:
    <h1>pytumblr</h1>
    <img src="http://b.hatena.ne.jp/entry/image/http://mattn.appspot.com/tumblr/" title="はてなブックマーク" />
    <div id="content">
$if res['name']:
    <h2>$res['name']'s tumblr</h2>
    $for post in res['posts']:
        $if post['type'] == 'photo':
            <b>PHOTO:</b>$:post['text']<br />
            <blockquote><img src="$post['link']" /></blockquote>
        $elif post['type'] == 'link':
            <b>LINK:</b><a href="$post['link']">$:post['text']</a><br />
            <blockquote class="link">$:post['desc']</blockquote>
        $elif post['type'] == 'quote':
            <b>QUOTE:</b><br />
            <blockquote class="quote">
                $:post['text']<br />
                <cite>$:post['link']</cite>
            </blockquote>
    </div>
    <hr clear="all" />
    <p style="text-align: center">provided by <a href="http://mattn.kaoriya.net">mattn</a>, hosted on google app server.</p>
</body>
</html>
このweb.pyのテンプレートって癖があってpythonインデント方式なのですが、これってpreとかcodeで先頭が入っちゃったら不味いんじゃないかと思ったり...
解決方あるのか、調べてみます。pytumblrで面白いもの作ってみて下さい。

みんなのPython みんなのPython
柴田 淳
ソフトバンククリエイティブ 単行本 / ¥50 (2006年08月22日)
 
発送可能時間:

Posted at by



2008/04/21


なるべくdictぽく扱えるように作ってみました。
WedataオブジェクトのコンストラクタにAPIKEYを掘り込んで操作します。
api = Wedata('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
databases = api.databases()
for database in databases:
  print database
  for key in database.keys():
    print " %s=%s" % (key, database[key])
  print
print
また、AutoPagerizeのデータベースであれば以下の様にdataプロパティからの属性参照の様に書く事も出来ます。
database = api.database('AutoPagerize')
print "%s : %s" % (database.name, database.description)
items = database.items()
for item in database.items():
  print " %s" % item.data.pageElement
  print " %s" % item.data.insertBefore
  print
Database.create_databaseの戻り値にはデータベース名(キー名)、Item.create_itemの戻り値にはアイテムIDが戻ります。このIDを使ってDatabase.delete_datebaseおよびItem.dalete_itemを呼び出す事が出来ます。
dbid = api.create_database('my_example_database', 'my_example_database', ['name', 'description'], ['value', 'xpath'], False)
api.delete_database(dbid)
今のところGoogle App Engineには対応していません。Google App Engine上での用途を見付けたら対応するかもしれません。
いつものようにコードはcodereposに置いてあります。
/lang/python/wedata
Posted at by