<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-2144470051777636547</id><updated>2011-11-28T08:34:54.379+09:00</updated><category term='App Engine'/><category term='Google apps'/><category term='Werkzeug'/><category term='japanese'/><category term='appengine'/><category term='python'/><category term='web'/><category term='Kay'/><category term='kay_fw'/><category term='Maps API'/><category term='community'/><category term='GAE'/><category term='IO2008'/><category term='pyjamas'/><category term='appengine python websockets'/><category term='mercurial'/><category term='google'/><title type='text'>Moon blue diary</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>41</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-2000225345185145945</id><published>2010-05-14T15:39:00.002+09:00</published><updated>2010-05-14T17:04:29.998+09:00</updated><title type='text'>Now Kay Framework has GAETestBase bundled!</title><content type='html'>What I've been working on recently is to incorporate &lt;a href="http://takashi-matsuo.blogspot.com/2010/05/introducing-gaetestbase.html"&gt;GAETestBase&lt;/a&gt; to &lt;a href="http://code.google.com/p/kay-framework/"&gt;Kay Framework&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;I've just finished the adapting work and bundled GAETestBase works very nicely with kay framework. You just need to change the base class of your testcase to "kay.ext.testutils.gae_test_base.GAETestBase" like:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;from kay.ext.testutils.gae_test_base import GAETestBase&lt;br /&gt;class DummyTest1(GAETestBase):&lt;br /&gt;  #...&lt;br /&gt;  def test_foo(self):&lt;br /&gt;    # ...&lt;br /&gt;    # ...&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;By default, when you run your tests via gaeunit, the web testrunner will collect testcases from all of your applications defined in settings.INSTALLED_APPS variable. For now, you cannot change this behavior, but in the near future, I'll add a feature for configuring which test to run via the gaeunit web testrunner.&lt;br /&gt;&lt;br /&gt;The recent version of Kay has a url mapping for web testrunner, but your app.yaml may lack of it. In such a case, please add following entry to your app.yaml:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;- url: /_ah/test.*&lt;br /&gt;  script: kay/ext/testutils/gaeunit.py&lt;br /&gt;  login: admin&lt;/pre&gt;&lt;br /&gt;As you can see, you can run your test by visiting a URL like "/_ah/test".&lt;br /&gt;&lt;br /&gt;As a note, all of your tests will be called in parallel, so it might leads to unexpected test failures. So you need to write your test functions independent from other tests.&lt;br /&gt;&lt;br /&gt;Please see &lt;a href="http://takashi-matsuo.blogspot.com/2010/05/introducing-gaetestbase.html"&gt;my previous post&lt;/a&gt; for details about how to configure GAETestBase.&lt;br /&gt;&lt;br /&gt;Happy testing!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-2000225345185145945?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/2000225345185145945/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=2000225345185145945' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/2000225345185145945'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/2000225345185145945'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2010/05/now-kay-framework-has-gaetestbase.html' title='Now Kay Framework has GAETestBase bundled!'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-7286171759067073087</id><published>2010-05-14T14:30:00.002+09:00</published><updated>2010-05-14T14:45:42.789+09:00</updated><title type='text'>How to download souce code of your application written with kay</title><content type='html'>Let me introduce a small trivia to you. What if your harddisk crashes and you lost your source code, hopefully, you might use some source code management system such as cvs, subversion, mercurial or git, in such a case, you can retrieve your source code back. But what if the crash occurs before your first commit, or your repository also crashes?&lt;br /&gt;&lt;br /&gt;Don't give up!&lt;br /&gt;&lt;br /&gt;Recent version of Kay Framework has remote_api and deferred handler by default, so there is still a chance &amp;nbsp;for retrieving it back to you.&lt;br /&gt;&lt;br /&gt;This idea is originally come from &lt;a href="http://stackoverflow.com/questions/2479087/can-i-restore-my-source-code-that-has-been-uploaded-into-google-appengine/"&gt;this thread&lt;/a&gt; in stackoverflow.&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;First, create a brand new project with kay's manage.py script and edit your app.yaml. Just put your application-id on the top of the file.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;$ python ~/work/kay/manage.py startproject downloadcode&lt;br /&gt;Running on Kay-0.10.0&lt;br /&gt;Finished creating new project: downloadcode.&lt;br /&gt;$ cd downloadcode&lt;br /&gt;$ vi settings.py&lt;br /&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;Second, create a file named "restore_code.py" as follows:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;import os&lt;br /&gt;from google.appengine.ext import db&lt;br /&gt;&lt;br /&gt;expr = """&lt;br /&gt;[type(&lt;br /&gt;    'CodeFile',&lt;br /&gt;    (__import__('google.appengine.ext.db').appengine.ext.db.Expando,),&lt;br /&gt;    {})(&lt;br /&gt;        name=dp+'/'+fn,&lt;br /&gt;        data=__import__('google.appengine.ext.db').appengine.ext.db.Text(&lt;br /&gt;            open(dp + '/' + fn).read()&lt;br /&gt;        )&lt;br /&gt;    ).put() if not dp.startswith('../kay') else None&lt;br /&gt; for dp, dns, fns in __import__('os').walk('..')&lt;br /&gt; for fn in fns]&lt;br /&gt;"""&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;class CodeFile(db.Model):&lt;br /&gt;  name = db.StringProperty(required=True)&lt;br /&gt;  data = db.TextProperty(required=True)&lt;br /&gt;&lt;br /&gt;  @classmethod&lt;br /&gt;  def kind(cls):&lt;br /&gt;    return cls.__name__&lt;br /&gt;&lt;br /&gt;def restore():&lt;br /&gt;  for cf in CodeFile.all():&lt;br /&gt;    fpath = cf.name.replace("../", "./_restored_files/")&lt;br /&gt;    dirname = os.path.dirname(fpath)&lt;br /&gt;    if not os.path.exists(dirname):&lt;br /&gt;      os.makedirs(dirname)&lt;br /&gt;    fh = open(fpath, "w")&lt;br /&gt;    fh.write(cf.data)&lt;br /&gt;    fh.close()&lt;br /&gt;&lt;br /&gt;def scan():&lt;br /&gt;  from google.appengine.ext.deferred import defer&lt;br /&gt;  defer(eval, expr)&lt;br /&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;Next, connect production server with "python manage.py rshell command."&lt;br /&gt;&lt;pre class="prettyprint"&gt;$ python manage.py rshell&lt;br /&gt;Running on Kay-0.10.0&lt;br /&gt;Username:matsuo.takashi&lt;br /&gt;Password:&lt;br /&gt;Interactive Kay Shell with RemoteDatastore. &lt;br /&gt;-----------------WARNING--------------------&lt;br /&gt;&lt;br /&gt;Please be careful in this console session.&lt;br /&gt;&lt;br /&gt;-----------------WARNING--------------------&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;In [1]: import restore_code&lt;br /&gt;In [2]: restore_code.scan()&lt;/pre&gt;restore_code.scan() will scan filesystems on the production and create CodeFile entities in the datastore. This work will be done by deferred handler, so you may need to wait a bit, but the completion of this process won't be notified. So why don't you take a break here and have a cup of coffee.&lt;/li&gt;&lt;li&gt;Lastly, call "restore_code.restore()" function.&lt;br /&gt;&lt;pre class="prettyprint"&gt;In [3]: restore_code.restore()&lt;br /&gt;In [4]:&lt;/pre&gt;&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;Then, you can have your entire source code except for static files and configuration files(such as app.yaml, index.yaml, etc..) in a directory named "_restored_files".&lt;br /&gt;&lt;br /&gt;Yey, now you've got your code back!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-7286171759067073087?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/7286171759067073087/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=7286171759067073087' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7286171759067073087'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7286171759067073087'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2010/05/how-to-download-souce-code-of-your.html' title='How to download souce code of your application written with kay'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-1530676474510224034</id><published>2010-05-13T17:10:00.005+09:00</published><updated>2010-05-14T17:08:57.121+09:00</updated><title type='text'>Introducing GAETestBase</title><content type='html'>A Japanese highly skilled engineer &lt;a href="http://d.hatena.ne.jp/tagomoris/"&gt;tago-san&lt;/a&gt; wrote an excellent module named GAETestBase. I'd like to introduce GAETestBase to you all. This entry is basically an English translation of &lt;a href="http://d.hatena.ne.jp/tagomoris/20100512/1273671773"&gt;this article of his&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;GAETestBase is a useful subclass of the standard unittest.TestCase. You can just write your test classes as usual by just extending GAETestBase class for utilizing this modules functionality.&lt;br /&gt;&lt;br /&gt;GAETestBase can be downloaded at &lt;a href="http://code.google.com/p/appengine-py-testhelper/source/browse/trunk/gae_test_base.py"&gt;Google Code Repository&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h1&gt;What can I do with this module?&lt;/h1&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;Run your tests via CLI&lt;/li&gt;&lt;ul&gt;&lt;li&gt;You can run your tests without setting up necessary environments (e.g. stubs, &amp;nbsp;env, etc..)&lt;/li&gt;&lt;li&gt;Tests will be use remote_api connection for accessing production datastore if you configure to do so in your TestCase.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Run your tests via GAEUnit&lt;/li&gt;&lt;ul&gt;&lt;li&gt;GAEUnit is a web-based test runner hosted at: &lt;a href="http://code.google.com/p/gaeunit/"&gt;GAEUnit Google Code&lt;/a&gt;&lt;/li&gt;&lt;li&gt;You can run your test with web-browser in your development environment.&lt;/li&gt;&lt;li&gt;&lt;b&gt;You can also run your tests with web-browser in your production environment. The test will be invoked under real production services(not memory based DatastoreFileStub) if you configure to do so.&lt;/b&gt;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Run your tests with special overridden kind() method.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;You can use this overridden kind method just in your tests without any changes in your code.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;For example, entities of &amp;nbsp;a model class "MyModel" will be stored as kind "t_MyModel".&lt;/li&gt;&lt;li&gt;Be careful with this special kind method in your TestCase. Please see an example bellow.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;Key.from_path('MyModel', id)       # NG!&lt;br /&gt;Key.from_path(MyModel.kind(), id)  # OK!&lt;/pre&gt;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Especially useful with tests via remote_api and tests on production(GAEUnit) because you can run your tests without any data pollution.&lt;/li&gt;&lt;li&gt;Of course, you can suppress this behavior by configuration&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Clean up all the kinds that are used in your tests.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;You can delete all the kinds used in your test after running your test.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;Only the kinds which is accessed in a particular TestCase, will be deleted.&lt;/li&gt;&lt;li&gt;*accessed* here includes just reading. So if you read a existing kind, the kind will become a target of deletion.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;&lt;b&gt;&lt;span class="Apple-style-span" style="color: red;"&gt;CAUTION:&lt;/span&gt; This feature could be very dangerous if you disable overridden kind&amp;nbsp;&lt;/b&gt;&lt;b&gt;method.&lt;/b&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;ul&gt;&lt;ul&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/div&gt;&lt;h1&gt;Let's do it&lt;/h1&gt;&lt;h3&gt;Download necessary files and deployment&lt;/h3&gt;You need to do following three things before start writing your tests.&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Deploy gaeunit.py&lt;/li&gt;&lt;ul&gt;&lt;li&gt;Download the file and put it into your project directory.&lt;/li&gt;&lt;li&gt;You need to re-write this file for changing _LOCAL_TEST_DIR if your tests are not placed in test/*.py.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Add two entries to your app.yaml&lt;/li&gt;&lt;ul&gt;&lt;li&gt;Configure a gaeunit handler and remote_api handler as follows:&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;- url: /remote_api&lt;br /&gt;  script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py&lt;br /&gt;  login: admin&lt;br /&gt;- url: /test.*&lt;br /&gt;  login: admin  # This is important if you deploy the test directory in production!&lt;br /&gt;  script: gaeunit.py&lt;/pre&gt;&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Create "test" directory and put gae_test_base.py into it, and edit constant variables in the file.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;You need to configure following variables:&lt;/li&gt;&lt;ul&gt;&lt;li&gt;GAE_HOME: a path for google appengine SDK&lt;/li&gt;&lt;li&gt;PROJECT_HOME: a path for your target project&lt;/li&gt;&lt;li&gt;APP_ID: your application id&lt;/li&gt;&lt;li&gt;REMOTE_API_ENTRY_POINT: a path section of the URI of remote_api (e.g. "/remote_api" for above example).&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/ol&gt;&lt;h3&gt;Run your test in local env.&lt;/h3&gt;Firstly, let's try running your test for local environment. This is a pretty ordinary test. "test_defs" here is just for model definition, so replace it with actual module with yours.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;from gae_test_base import *&lt;br /&gt;&lt;br /&gt;from test_defs import *&lt;br /&gt;from google.appengine.ext import db&lt;br /&gt;&lt;br /&gt;class DummyTest1(GAETestBase):&lt;br /&gt;&lt;br /&gt;    def test_put(self):&lt;br /&gt;        x1 = XEntity(x="x1")&lt;br /&gt;        k1 = x1.put()&lt;br /&gt;        self.assertEqual(db.get(k1).x, "x1")&lt;br /&gt;&lt;br /&gt;    def test_tx(self):&lt;br /&gt;        def tx1(x2key, x2, x3):&lt;br /&gt;            x2 = XEntity(key=x2key, x=x2)&lt;br /&gt;            x3 = XEntity(x=x3, parent=x2)&lt;br /&gt;            x2.put()&lt;br /&gt;            x3.put()&lt;br /&gt;            return (x2.key(), x3.key())&lt;br /&gt;        x2k = db.allocate_ids(db.Key.from_path(XEntity.kind(), -1), 1)[0]&lt;br /&gt;        k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(XEntity.kind(), int(x2k)), "x2", "x3")&lt;br /&gt;        self.assertEqual(db.get(k2).x, "x2")&lt;br /&gt;        self.assertEqual(db.get(k3).x, "x3")&lt;/pre&gt;&lt;br /&gt;Actual tests are very simple as follows:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;test_put&lt;/li&gt;&lt;ul&gt;&lt;li&gt;Create a new entity and put() it.&lt;/li&gt;&lt;li&gt;Getting an entity with returned key again, and compare values of two entities.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;test_tx&lt;/li&gt;&lt;ul&gt;&lt;li&gt;Define a following function "tx1" for transaction&lt;/li&gt;&lt;ul&gt;&lt;li&gt;receives a key for a root entity, a value for a property of the root entity, and a value for a property of the child entity as its arguments.&lt;/li&gt;&lt;li&gt;creates a new root entity and a child entity.&lt;/li&gt;&lt;li&gt;puts'em together&lt;/li&gt;&lt;li&gt;returns those two keys&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Get an id for a new root entity by invoking allocate_ids&lt;/li&gt;&lt;li&gt;Run tx1 under a transaction&lt;/li&gt;&lt;li&gt;Get two entities with the key returned from transaction call, and compare property values with expected one.&lt;/li&gt;&lt;/ul&gt;&lt;/ol&gt;Here is an example output when running this testcase:  &lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;$ python2.5 dummy_test.py &lt;br /&gt;..&lt;br /&gt;----------------------------------------------------------------------&lt;br /&gt;Ran 2 tests in 0.020s&lt;br /&gt;&lt;br /&gt;OK&lt;br /&gt;$&lt;/pre&gt;&lt;br /&gt;Here is an example screenshot when running this testcase via GAEUnit:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://img.f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512213431.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="351" src="http://img.f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512213431.png" width="640" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;h3&gt;Run your test via remote_api or on production&lt;/h3&gt;Next, let's add a new testcase that uses the production datastore as its backend. Here is an example configuration for using remote_api via CLI, and using production datastore when deployed on appspot.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;class DummyTest2(GAETestBase):&lt;br /&gt;    USE_PRODUCTION_STUBS = True&lt;br /&gt;    USE_REMOTE_STUBS = True&lt;br /&gt;&lt;br /&gt;    def test_put(self):&lt;br /&gt;        y1 = YEntity(y="y1")&lt;br /&gt;        k1 = y1.put()&lt;br /&gt;        self.assertEqual(db.get(k1).y, "y1")&lt;br /&gt;&lt;br /&gt;    def test_tx(self):&lt;br /&gt;        def tx1(y2key, y2, y3):&lt;br /&gt;            y2 = YEntity(key=y2key, y=y2)&lt;br /&gt;            y3 = YEntity(y=y3, parent=y2)&lt;br /&gt;            y2.put()&lt;br /&gt;            y3.put()&lt;br /&gt;            return (y2.key(), y3.key())&lt;br /&gt;        y2k = db.allocate_ids(db.Key.from_path(YEntity.kind(), -1), 1)[0]&lt;br /&gt;        k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(YEntity.kind(), int(y2k)), "y2", "y3")&lt;br /&gt;        self.assertEqual(db.get(k2).y, "y2")&lt;br /&gt;        self.assertEqual(db.get(k3).y, "y3")&lt;/pre&gt;&lt;br /&gt;You can configure all the behavior by defining class attributes on your extended TestCase classes. The default value of all the configurable attributes is False when omitted. This new testcase is almost the same as the previous testcase except for a target model class.&lt;br /&gt;&lt;br /&gt;Here is an example output via CLI:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;$ python2.5 dummy_test.py &lt;br /&gt;....&lt;br /&gt;----------------------------------------------------------------------&lt;br /&gt;Ran 4 tests in 6.578s&lt;br /&gt;&lt;br /&gt;OK&lt;br /&gt;$&lt;/pre&gt;&lt;br /&gt;Now you can recognize that, this time, two testcases are called. You may asked your username and password on test that uses remote_api, though its omitted because there is a valid appcfg_cookies.&lt;br /&gt;&lt;br /&gt;Here is an output via GAEUnit:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512213433.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="351" src="http://f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512213433.png" width="640" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;You can also run these tests on appspot by just visiting http://app-id.appspot.com/test. After running these tests on appspot, you can see that entities which are used in your test are stored in the production datastore. A kind of these entities is "t_YEntity" (specially prefixed for test) like below:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512212829.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="250" src="http://f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512212829.png" width="640" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;h3&gt;Cleaning up entities in your tests&lt;/h3&gt;If you don't like entities created in your tests remains, you can write a testcase which deletes entities which are accessed in your tests like:&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;class DummyTest3(GAETestBase):&lt;br /&gt;    USE_PRODUCTION_STUBS = True&lt;br /&gt;    USE_REMOTE_STUBS = True&lt;br /&gt;    CLEANUP_USED_KIND = True&lt;br /&gt;&lt;br /&gt;    def test_put(self):&lt;br /&gt;        z1 = ZEntity(z="z1")&lt;br /&gt;        k1 = z1.put()&lt;br /&gt;        self.assertEqual(db.get(k1).z, "z1")&lt;br /&gt;&lt;br /&gt;    def test_tx(self):&lt;br /&gt;        def tx1(z2key, z2, z3):&lt;br /&gt;            z2 = ZEntity(key=z2key, z=z2)&lt;br /&gt;            z3 = ZEntity(z=z3, parent=z2)&lt;br /&gt;            z2.put()&lt;br /&gt;            z3.put()&lt;br /&gt;            return (z2.key(), z3.key())&lt;br /&gt;        z2k = db.allocate_ids(db.Key.from_path(ZEntity.kind(), -1), 1)[0]&lt;br /&gt;        k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(ZEntity.kind(), int(z2k)), "z2", "z3")&lt;br /&gt;        self.assertEqual(db.get(k2).z, "z2")&lt;br /&gt;        self.assertEqual(db.get(k3).z, "z3")&lt;/pre&gt;&lt;br /&gt;You can run these test via CLI, &lt;a href="http://f.hatena.ne.jp/tagomoris/20100512213434"&gt;via GAEUnit(dev)&lt;/a&gt;, and &lt;a href="http://f.hatena.ne.jp/tagomoris/20100512213435"&gt;via GAEUnit(prod)&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;After running your tests, you can see there is no "t_ZEntity" on the datastore at all:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512212831.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="275" src="http://f.hatena.ne.jp/images/fotolife/t/tagomoris/20100512/20100512212831.png" width="640" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;h3&gt;Per testcase cutomizing&lt;/h3&gt;Here is an example for customizing kind prefix and clean up behavior.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;class DummyTest4(GAETestBase):&lt;br /&gt;    USE_PRODUCTION_STUBS = True&lt;br /&gt;    USE_REMOTE_STUBS = True&lt;br /&gt;    CLEANUP_USED_KIND = False&lt;br /&gt;    KIND_PREFIX_IN_TEST = 'test'&lt;br /&gt;&lt;br /&gt;    def test_put(self):&lt;br /&gt;        z1 = ZEntity(z="z1")&lt;br /&gt;        k1 = z1.put()&lt;br /&gt;        self.assertEqual(db.get(k1).z, "z1")&lt;br /&gt;&lt;br /&gt;    def test_tx(self):&lt;br /&gt;        def tx1(z2key, z2, z3):&lt;br /&gt;            z2 = ZEntity(key=z2key, z=z2)&lt;br /&gt;            z3 = ZEntity(z=z3, parent=z2)&lt;br /&gt;            z2.put()&lt;br /&gt;            z3.put()&lt;br /&gt;            return (z2.key(), z3.key())&lt;br /&gt;        z2k = db.allocate_ids(db.Key.from_path(ZEntity.kind(), -1), 1)[0]&lt;br /&gt;        k2, k3 = db.run_in_transaction(tx1, db.Key.from_path(ZEntity.kind(), int(z2k)), "z2", "z3")&lt;br /&gt;        self.assertEqual(db.get(k2).z, "z2")&lt;br /&gt;        self.assertEqual(db.get(k3).z, "z3")&lt;/pre&gt;&lt;br /&gt;After running this test, you can see "test_ZEntity" is stored in the datastore(ruled out from deletion).&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Accessing the real data on production&lt;/h3&gt;&lt;br /&gt;Here is an example testcase for accessing the real data on prod.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;class DummyTest5(GAETestBase):&lt;br /&gt;    USE_PRODUCTION_STUBS = True&lt;br /&gt;    USE_REMOTE_STUBS = True&lt;br /&gt;    CLEANUP_USED_KIND = False&lt;br /&gt;    KIND_NAME_UNSWAPPED = True&lt;br /&gt;&lt;br /&gt;    def test_put(self):&lt;br /&gt;        count = PEntity.all().count()&lt;br /&gt;        p1 = PEntity(i=count)&lt;br /&gt;        k1 = p1.put()&lt;br /&gt;        px = db.get(k1)&lt;br /&gt;        self.assertEqual(px.i, count)&lt;/pre&gt;&lt;br /&gt;If KIND_NAME_UNSWAPPED is set to True, you can suppress overriding kind method on db.Model. So, if you turn on USE_REMOTE_STUBS or USE_PRODUCTION_STUBS, your tests can access the real data on the production datastore.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;How to use this module with Kay Framework?&lt;/h3&gt;Now Kay has GAETestBase bundled, so you can use it out of the box!&lt;br /&gt;For more details, please see &lt;a href="http://takashi-matsuo.blogspot.com/2010/05/now-kay-framework-has-gaetestbase.html"&gt;this article&lt;/a&gt; as well.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Caution&lt;/h3&gt;TODO: translate&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Far beyond&lt;/h3&gt;TODO: translate&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-1530676474510224034?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/1530676474510224034/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=1530676474510224034' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1530676474510224034'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1530676474510224034'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2010/05/introducing-gaetestbase.html' title='Introducing GAETestBase'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-1650078204781461530</id><published>2010-05-08T08:20:00.006+09:00</published><updated>2010-05-20T10:39:02.933+09:00</updated><title type='text'>Creating an app for Google Apps Marketplace with Kay - part 1 (revised)</title><content type='html'>&lt;span style="font-size:200%"&gt;This entiry is obsoleted because Appengine SDK-1.3.4 was released, and it has a capability for OpenID authentication. I'll definitely start figuring an easy way for writing Google App Engine application for Google Apps Marketplace soon, so please wait a bit.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;p&gt;I've just implemented an authentication mechanism in Kay framework for Google Apps MarketPlace. So let me introduce how to deploy a marketplace application with Kay(repository version). &lt;br /&gt;&lt;br /&gt;&lt;p style="font-color: #822"&gt;I have made a few more improvements in authentication system of Kay Framework, so I slightly change this article at 4:20PM on May 13th 2010JST.&lt;br /&gt;&lt;h1&gt;1. Create an app-id&lt;/h1&gt;&lt;p&gt;In this example, I've created a brand new application slot called 'marketplace-app'.&lt;br /&gt;&lt;h1&gt;2. Register a listing on the MarketPlace&lt;/h1&gt;&lt;p&gt;Firstly, you need to register yourself as a MarketPlace Vendor.&lt;br /&gt;http://www.google.com/enterprise/marketplace/&lt;br /&gt;&lt;p&gt;Click a link 'Become a vendor' in the bottom of the page above and you'll get an instruction.&lt;br /&gt;&lt;p&gt;Actually you need to click a link 'Sign in' at top-right corner of the page above, and fill the form.&lt;br /&gt;&lt;p&gt;Then, you can create your listing. Click a button 'Create a new listing' in your Vendor Profile page.&lt;br /&gt;&lt;p&gt;In this example, lets create an installable app, so check the first checkbox 'My product may be directly installed into Google Apps domains'.&lt;br /&gt;&lt;p&gt;You can fill other fields arbitrarily except for the field 'Manifest'.&lt;br /&gt;&lt;p&gt;Manifest xml here:&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8" ?&amp;gt;&lt;br /&gt;&amp;lt;ApplicationManifest xmlns="http://schemas.google.com/ApplicationManifest/2009"&amp;gt;&lt;br /&gt;    &lt;br /&gt;  &amp;lt;!-- Support info to show in the marketplace &amp;amp; control panel --&amp;gt;&lt;br /&gt;  &amp;lt;Support&amp;gt;&lt;br /&gt;    &amp;lt;!-- URL for application setup as an optional redirect during the install --&amp;gt;&lt;br /&gt;    &amp;lt;Link rel="setup" href="https://marketplace-app.appspot.com/a/${DOMAIN_NAME}/setup" /&amp;gt;&lt;br /&gt;    &lt;br /&gt;    &amp;lt;!-- URL for application configuration, accessed from the app settings page in the control panel --&amp;gt;&lt;br /&gt;    &amp;lt;Link rel="manage" href="https://marketplace-app.appspot.com/a/${DOMAIN_NAME}/admin" /&amp;gt;&lt;br /&gt;&lt;br /&gt;    &amp;lt;!-- URL explaining how customers get support. --&amp;gt;&lt;br /&gt;    &amp;lt;Link rel="support" href="https://marketplace-app.appspot.com/a/${DOMAIN_NAME}/support" /&amp;gt;&lt;br /&gt;  &lt;br /&gt;    &amp;lt;!-- URL that is displayed to admins during the deletion process, to specify policies such as data retention, how to claim accounts, etc. --&amp;gt;&lt;br /&gt;    &amp;lt;Link rel="deletion-policy" href="https://marketplace-app.appspot.com/deletion-policy" /&amp;gt;&lt;br /&gt;  &amp;lt;/Support&amp;gt;&lt;br /&gt;    &lt;br /&gt;  &amp;lt;!-- Name and description pulled from message bundles --&amp;gt;&lt;br /&gt;  &amp;lt;Name&amp;gt;Marketplace sample application&amp;lt;/Name&amp;gt;&lt;br /&gt;  &amp;lt;Description&amp;gt;A simple application for the marketplace&amp;lt;/Description&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;!-- Show this link in Google's universal navigation for all users --&amp;gt;&lt;br /&gt;  &amp;lt;Extension id="navLink" type="link"&amp;gt;&lt;br /&gt;    &amp;lt;Name&amp;gt;Sample&amp;lt;/Name&amp;gt;&lt;br /&gt;    &amp;lt;Url&amp;gt;https://marketplace-app.appspot.com/a/${DOMAIN_NAME}/start&amp;lt;/Url&amp;gt;&lt;br /&gt;    &amp;lt;!-- This app also uses the Calendar API --&amp;gt;&lt;br /&gt;    &amp;lt;Scope ref="calendarFeed"/&amp;gt;&lt;br /&gt;  &amp;lt;/Extension&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;!-- Declare our OpenID realm so our app is white listed --&amp;gt;&lt;br /&gt;  &amp;lt;Extension id="realm" type="openIdRealm"&amp;gt;&lt;br /&gt;    &amp;lt;Url&amp;gt;https://marketplace-app.appspot.com/&amp;lt;/Url&amp;gt;&lt;br /&gt;  &amp;lt;/Extension&amp;gt;&lt;br /&gt;&lt;br /&gt;  &amp;lt;!-- Need access to the Calendar feed --&amp;gt;&lt;br /&gt;  &amp;lt;Scope id="calendarFeed"&amp;gt;&lt;br /&gt;    &amp;lt;Url&amp;gt;https://www.google.com/calendar/feeds/&amp;lt;/Url&amp;gt;&lt;br /&gt;    &amp;lt;Reason&amp;gt;This application shows the next Calendar event.&amp;lt;/Reason&amp;gt;&lt;br /&gt;  &amp;lt;/Scope&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;/ApplicationManifest&amp;gt;&lt;/pre&gt;&lt;p&gt;In this example, there is a calendar feed setting for 2 legged OAuth access scope. For more details with a Manifest file format, please see:&lt;br /&gt;http://code.google.com/googleapps/marketplace/manifest.html&lt;br /&gt;&lt;p&gt;After adding your listings, you will see a preview page for your app. There is an attractive 'Add it now' button on the right, but of course, you need to implement your application before adding this app.&lt;br /&gt;&lt;p&gt;Instead, click a link 'My Vendor Profile' on the top-right corner and you can see your listings with a link 'View OAuth Consumer Key'.&lt;br /&gt;&lt;h1&gt;3. Implementation&lt;/h1&gt;&lt;p&gt;Let's create a new project with Kay management script:&lt;br /&gt;&lt;pre&gt;$ python /some/where/kay/manage.py startproject marketplace-app&lt;br /&gt;Running on Kay-0.10.0&lt;br /&gt;Finished creating new project: marketplace-app.&lt;br /&gt;$ cd marketplace-app&lt;br /&gt;$ python manage.py startapp core&lt;/pre&gt;&lt;p&gt;settings.py:&lt;br /&gt;&lt;pre class="prettyprint"&gt;INSTALLED_APPS = (&lt;br /&gt;  'core',&lt;br /&gt;  'kay.ext.gaema',&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;APP_MOUNT_POINTS = {&lt;br /&gt;  'core': '/',&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;MIDDLEWARE_CLASSES = (&lt;br /&gt;  'kay.sessions.middleware.SessionMiddleware',&lt;br /&gt;  'kay.auth.middleware.AuthenticationMiddleware',&lt;br /&gt;)&lt;br /&gt;AUTH_USER_BACKEND = "kay.auth.backends.gaema.GAEMABackend"&lt;br /&gt;GAEMA_USER_MODEL = "core.models.User"&lt;br /&gt;&lt;br /&gt;GAEMA_SECRETS = {&lt;br /&gt;  'google_consumer_key': 'your consumer key here',&lt;br /&gt;  'google_consumer_secret': 'your consumer key secret here',&lt;br /&gt;}&lt;br /&gt;IS_MARKETPLACE_APP = True&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Replace GAEMA_SECRETS with actual values. You can see consumer key and consumer key secret with clicking the link 'View OAuth Consumer Key' mentioned above. The last attribute 'IS_MARKETPLACE_APP' is important for this authentication system work normaly.&lt;br /&gt;&lt;p&gt;core/models.py:&lt;br /&gt;&lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-                                                                                                                                          &lt;br /&gt;# core.models                                                                                                                                                    &lt;br /&gt;&lt;br /&gt;from google.appengine.ext import db&lt;br /&gt;from kay.ext.gaema.models import GAEMAUser&lt;br /&gt;&lt;br /&gt;# Create your models here.                                                                                                                                       &lt;br /&gt;&lt;br /&gt;class User(GAEMAUser):&lt;br /&gt;  pass&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Information of users will be stored into this model.&lt;br /&gt;&lt;p&gt;core/urls.py:&lt;br /&gt;&lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# core.urls&lt;br /&gt;#&lt;br /&gt;&lt;br /&gt;from kay.routing import (&lt;br /&gt;  ViewGroup, Rule&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;view_groups = [&lt;br /&gt;  ViewGroup(&lt;br /&gt;    Rule('/a/&amp;lt;domain_name&amp;gt;/setup', endpoint='domain_setup',&lt;br /&gt;         view='core.views.domain_setup'),&lt;br /&gt;&lt;br /&gt;    Rule('/a/&amp;lt;domain_name&amp;gt;/admin', endpoint='domain_admin',&lt;br /&gt;         view='core.views.domain_admin'),&lt;br /&gt;&lt;br /&gt;    Rule('/a/&amp;lt;domain_name&amp;gt;/support', endpoint='domain_support',&lt;br /&gt;         view='core.views.domain_support'),&lt;br /&gt;&lt;br /&gt;    Rule('/a/&amp;lt;domain_name&amp;gt;/start', endpoint='domain_start',&lt;br /&gt;         view='core.views.domain_start'),&lt;br /&gt;&lt;br /&gt;    Rule('/deletion_policy', endpoint='deletion_policy',&lt;br /&gt;         view='core.views.deletion_policy'),&lt;br /&gt;  )&lt;br /&gt;]&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;core/views.py:&lt;br /&gt;&lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-                                                                                                                                          &lt;br /&gt;"""                                                                                                                                                              &lt;br /&gt;core.views                                                                                                                                                       &lt;br /&gt;"""&lt;br /&gt;from werkzeug import (&lt;br /&gt;  unescape, redirect, Response,&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;from kay.utils import render_to_response&lt;br /&gt;from kay.ext.gaema.utils import get_gaema_user&lt;br /&gt;from kay.auth.decorators import login_required&lt;br /&gt;&lt;br /&gt;def index(request):&lt;br /&gt;  return render_to_response('core/index.html', {'message': 'Hello'})&lt;br /&gt;&lt;br /&gt;def domain_setup(request, domain_name):&lt;br /&gt;  callback = request.args.get('callback')&lt;br /&gt;  if callback is None:&lt;br /&gt;    return Response("No callback supplied.")&lt;br /&gt;  return redirect(callback)&lt;br /&gt;&lt;br /&gt;def domain_admin(request, domain_name):&lt;br /&gt;  return Response("%s administration." % domain_name)&lt;br /&gt;&lt;br /&gt;def domain_support(request, domain_name):&lt;br /&gt;  return Response("%s support." % domain_name)&lt;br /&gt;&lt;br /&gt;def deletion_policy(request):&lt;br /&gt;  return Response("Deletion policy.")&lt;br /&gt;&lt;br /&gt;@login_required&lt;br /&gt;def domain_start(request, domain_name):&lt;br /&gt;  return Response("%s start.\n%s" % (domain_name, request.user.raw_user_data))&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;A decorator 'login_required' takes care of OpenID/OAuth stuff, so you don't need to do anything at all. The user information is in request.user.raw_user_data.&lt;br /&gt;&lt;p&gt;Let's deploy it to appspot.&lt;br /&gt;&lt;pre&gt;$ python manage.py appcfg update&lt;/pre&gt;&lt;p&gt;You can check the application with visiting https://marketplace-app.appspot.com/a/example.com/start where example.com should be replaced with your actual domain. This domain must be registered with Google Apps and make sure that "Federated Login using OpenID" is turned on.&lt;br /&gt;&lt;p&gt;If the app is successfully deployed, you can see an OpenID authentication display. Let's move on.&lt;br /&gt;&lt;h1&gt;4. Add your app to your domain&lt;/h1&gt;&lt;p&gt;Go back to your Vendor Profile Page, and click your application title, then you will see the preview page of your application.&lt;br /&gt;&lt;p&gt;Let's click 'Add it now' button on the right, and enter your google apps domain, and click 'Go'. &lt;br /&gt;Follow an adding application wizard like 1) Agree to terms 2) Grant data access 3) External configuration 4) Enable the app, and wait for a while, and you can see a universal navigation link 'Sample' on the top left corner of your google apps applications.&lt;br /&gt;&lt;p&gt;If you click the link, you can silently signed into your application because Marketplace apps are whitelisted with a particular openid_realm.&lt;br /&gt;&lt;br /&gt;&lt;p&gt;In part 2 of this article, I'll show you how to access user's calendar data with gdata library.&lt;br /&gt;&lt;br /&gt;&lt;p&gt;To be continued..&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-1650078204781461530?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/1650078204781461530/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=1650078204781461530' title='4 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1650078204781461530'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1650078204781461530'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2010/05/creating-app-for-google-apps.html' title='Creating an app for Google Apps Marketplace with Kay - part 1 (revised)'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-831383039044917573</id><published>2009-12-24T22:18:00.003+09:00</published><updated>2009-12-24T22:52:09.366+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='appengine python websockets'/><title type='text'>Integrating websockets with appengine applications</title><content type='html'>Today, I've been struggling with an experimental implementation for a pseudo server push on appengine application. So let me share it with you.&lt;br /&gt;&lt;br /&gt;The only problem with appengine is that we can not utilize comet like capabilities on it because of its 30 seconds request limit.&lt;br /&gt;&lt;br /&gt;In this article, I use external websockets server for a pseudo server push on appengine. Here is the diagram(Click for bigger image).&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/_DkVkyz39PB0/SzNkw3NEeMI/AAAAAAAAD5Y/OEgTPskf50c/s1600-h/appengine-websockets-integration-diagram.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/_DkVkyz39PB0/SzNkw3NEeMI/AAAAAAAAD5Y/OEgTPskf50c/s640/appengine-websockets-integration-diagram.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;Let me explain this diagram a bit.&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;ol&gt;&lt;li&gt;When a client request for the content,&amp;nbsp;&lt;/li&gt;&lt;li&gt;appengine webapp will returns the newest contents with javascripts for establishing a new websockets connection to an external websockets server.&lt;/li&gt;&lt;li&gt;The browser opens a new websockets connection to the websockets server. This connection will remain as long as possible for notifying updates of the contents.&lt;/li&gt;&lt;li&gt;Another browser requests for updating the contents(e.g. Posting a new comment...etc...).&lt;/li&gt;&lt;li&gt;appengine webapp will save the state in the datastore, and give the newest contents to the client, notifying about this updates to the websockets server as well, simultaneously.&lt;/li&gt;&lt;li&gt;On the websockets server, every server process will receive the notification, and tell their clients that there is some update via the persistent websockets connection.&lt;/li&gt;&lt;li&gt;Now, the first browser knows that there is updated contents on the server, so (in this case) it makes an ajax request to the appengine webapp, and receives the newest contents.&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;I've implemented &lt;a href="http://websocketchat.appspot.com/"&gt;a simple chat with this&amp;nbsp;architecture&lt;/a&gt;. Please visit and try it if you'd like. I've tested it only with Chrome 4 or higher(including chromium).&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Now, let's walk through into the code. On the appengine side, when a new comment arives, I need to notify it to the websockets server, so I use urlfetch for this. Here is the code:&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;pre class="prettyprint"&gt;def index(request):&lt;br /&gt;  form = CommentForm()&lt;br /&gt;  if request.method == 'POST':&lt;br /&gt;    if form.validate(request.form):&lt;br /&gt;      if request.user.is_authenticated():&lt;br /&gt;        form.save(owner=request.user)&lt;br /&gt;      else:&lt;br /&gt;        form.save()&lt;br /&gt;      import time&lt;br /&gt;      urlfetch.fetch('http://mars.shehas.net/update_webhook.cgi?'&lt;br /&gt;                     + str(time.time()))&lt;br /&gt;      return redirect(url_for('chat/index'))&lt;br /&gt;  comments = Comment.all().order('-created').fetch(20)&lt;br /&gt;  return render_to_response('chat/index.html',&lt;br /&gt;                            {'form': form.as_widget(),&lt;br /&gt;                             'comments': comments})&lt;/pre&gt;&lt;br /&gt;The most important thing is that after a new comment is saved, the handler makes an urlfetch call to the external websockets server for notification. It is also important to add time.time() string representation to the url because without this, appengine urlfetch server may cache the response, and this urlfetch call will be useless as a webhook.&lt;br /&gt;&lt;br /&gt;On the client side, we have to create a websocket connection, and set appropriate callbacks on some events of the connection. I've wrote a new class for this.&lt;br /&gt;&lt;br /&gt;update_check_socket.js&lt;br /&gt;&lt;pre class="prettyprint"&gt;function UpdateCheckSocket(host, port, resource, statusElement, callback) {&lt;br /&gt;  this.host = host;&lt;br /&gt;  this.port = port;&lt;br /&gt;  this.resource = resource;&lt;br /&gt;  this.statusElement = statusElement;&lt;br /&gt;  this.callback = callback;&lt;br /&gt;  this.ws = new WebSocket("ws://"+this.host+":"+this.port+this.resource);&lt;br /&gt;  this.ws.onopen = function(e) {&lt;br /&gt;    statusElement.innerHTML='Web sockets connected';&lt;br /&gt;  };&lt;br /&gt;  this.ws.onmessage = function(e) {&lt;br /&gt;    var newDiv = document.createElement('div');&lt;br /&gt;    newDiv.innerHTML = e.origin + decodeURIComponent(e.data);&lt;br /&gt;    statusElement.insertBefore(newDiv, statusElement.firstChild);&lt;br /&gt;    if (decodeURIComponent(e.data) == 'UPDATED') {&lt;br /&gt;      callback();&lt;br /&gt;    }&lt;br /&gt;  };&lt;br /&gt;  this.ws.onclose = function(e) {&lt;br /&gt;    var newDiv = document.createElement('div');&lt;br /&gt;    newDiv.innerHTML = 'Web sockets closed';&lt;br /&gt;    statusElement.insertBefore(newDiv, statusElement.firstChild);&lt;br /&gt;  };&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;function UpdateCheckSocket_send(message) {&lt;br /&gt;  if(typeof(message) == 'undefined' || message =='') {&lt;br /&gt;    alert('no message...');&lt;br /&gt;    return;&lt;br /&gt;  }&lt;br /&gt;  this.ws.send(encodeURIComponent(message));&lt;br /&gt;}&lt;br /&gt;UpdateCheckSocket.prototype.send = UpdateCheckSocket_send;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;On the main html, there is a callback for retrieving the newest contents. In some cases, the connection will be closed unintentionally because some network routers might delete the NAT table when there has been no data &amp;nbsp;for few minutes. So there is also the code for avoiding this by sending 'NOOP' string to the server periodically.&lt;br /&gt;&lt;br /&gt;Here is &lt;a href="http://paste.shehas.net/show/30/"&gt;the code for main html&lt;/a&gt;(as long as I'm concerned, blogger could not handle html well).&lt;br /&gt;&lt;br /&gt;Ok. Let's go to the websockets side. On the external websockets server, I need to 1) accept update notification from appengine webapp(webhook handler), 2) handle websockets connection and notify the update to the client.&lt;br /&gt;&lt;br /&gt;Here is the code for the webhook.&lt;br /&gt;update_webhook.cgi&lt;br /&gt;&lt;pre class="prettyprint"&gt;#!/usr/bin/python&lt;br /&gt;&lt;br /&gt;import sqlite3&lt;br /&gt;import time&lt;br /&gt;import sys&lt;br /&gt;&lt;br /&gt;conn = sqlite3.connect("/tmp/test")&lt;br /&gt;c = conn.cursor()&lt;br /&gt;try:&lt;br /&gt;  c.execute('create table updated (t real)')&lt;br /&gt;  c.execute('insert into updated values (?)', (time.time(),))&lt;br /&gt;except Exception, e:&lt;br /&gt;  sys.stderr.write(str(e))&lt;br /&gt;c.execute('update updated set t = ?', (time.time(),))&lt;br /&gt;conn.commit()&lt;br /&gt;c.close()&lt;br /&gt;conn.close()&lt;br /&gt;&lt;br /&gt;print "Content-type: text/plain\n"&lt;br /&gt;print "OK"&lt;/pre&gt;&lt;br /&gt;This is a very simple script. Its an experimental implementation, so currently it does't check if the request really came from a particular appengine webapp. So do not use this code as is in any production environment.&lt;br /&gt;&lt;br /&gt;The last piece is websocket wsh script(I used &lt;a href="http://code.google.com/p/pywebsocket/"&gt;pywebsockets&lt;/a&gt; here).&lt;br /&gt;&lt;pre class="prettyprint"&gt;import sqlite3&lt;br /&gt;import time&lt;br /&gt;&lt;br /&gt;from mod_pywebsocket import msgutil&lt;br /&gt;&lt;br /&gt;CHECK_INTERVAL=1&lt;br /&gt;_UPDATE_SIGNAL='UPDATED'&lt;br /&gt;&lt;br /&gt;def web_socket_do_extra_handshake(request):&lt;br /&gt;  pass  # Always accept.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;def web_socket_transfer_data(request):&lt;br /&gt;  last_checked = time.time()&lt;br /&gt;  conn = sqlite3.connect("/tmp/test")&lt;br /&gt;  c = conn.cursor()&lt;br /&gt;  while True:&lt;br /&gt;    time.sleep(CHECK_INTERVAL)&lt;br /&gt;    c.execute('select t from updated')&lt;br /&gt;    r = c.fetchone()&lt;br /&gt;    if r and r[0] &gt; last_checked:&lt;br /&gt;      last_checked = r[0]&lt;br /&gt;      msgutil.send_message(request, _UPDATE_SIGNAL)&lt;/pre&gt;&lt;br /&gt;Here, I use sqlite3 for recording the last update time. Using sqlite3 might not be appropriate for the production environment either, again, this is just an experimental implementaion :-)&lt;br /&gt;&lt;br /&gt;Well that's about it. Actually it works, but I don't think this is the best approach. Maybe current implementation won't scale, it might be somewhat cumbersome to setup all of these complicated stuff. I hope I can make these set of code more sophisticated and general in the future, or I hope someone can write better code/architecture for similar purpose.&lt;br /&gt;&lt;br /&gt;Merry X'mas and happy new year :-)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-831383039044917573?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/831383039044917573/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=831383039044917573' title='9 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/831383039044917573'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/831383039044917573'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/12/integrating-websockets-with-appengine.html' title='Integrating websockets with appengine applications'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_DkVkyz39PB0/SzNkw3NEeMI/AAAAAAAAD5Y/OEgTPskf50c/s72-c/appengine-websockets-integration-diagram.png' height='72' width='72'/><thr:total>9</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-4251928415648004419</id><published>2009-12-12T08:42:00.003+09:00</published><updated>2009-12-12T15:13:12.748+09:00</updated><title type='text'>Using appstats with Kay</title><content type='html'>Guido &lt;a href="http://groups.google.com/group/google-appengine-python/browse_thread/thread/bff473631cd21159?pli=1"&gt;announced the preview release of appstats&lt;/a&gt;. This is a tool for visualizing call stats of RPC Calls on appengine that is very useful for improving application's performance.&lt;br /&gt;&lt;br /&gt;This tool consists of two parts; recording part and ui part. There are two ways for configuring the recording part. The first one is to use django middleware which appstats offers. Another way is to configure WSGI middleware. The former way is much easier, so I tried to utilize this django middleware with Kay.&lt;br /&gt;&lt;br /&gt;I have thought that I could easily utilize this middleware with small modifications because Kay has a middleware mechanism very similar to the django's one. Finally, and fortunately I can use this middleware without any modification. That's what I aimed for by implementing Kay's middleware mechanism as similarly as possible to django's one!&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;MIDDLEWARE_CLASSES = (&lt;br /&gt;  'appstats.recording.AppStatsDjangoMiddleware',&lt;br /&gt;  'kay.auth.middleware.AuthenticationMiddleware',&lt;br /&gt;)&lt;/pre&gt;&lt;br /&gt;Configuring the recording part is done by adding appstats.recording.AppStatsDjangoMiddleware to the MIDDLEWARE_CLASSES as above.&lt;br /&gt;&lt;br /&gt;Next I need to configure ui part. According to &lt;a href="http://docs.google.com/Doc?docid=0AQwBadvrXbuVZGZkM3p0czRfMTlkNjI1M2NjOQ&amp;amp;pli=1"&gt;the documentation of appstats&lt;/a&gt;, I added an entry to my app.yaml as follows.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;- url: /stats.*&lt;br /&gt;  script: appstats/ui.py&lt;br /&gt;  login: admin&lt;/pre&gt;&lt;br /&gt;This is nearly perfect, but a handlers for viewing source code didn't work correctly, so I needed to add these lines to the upper part of &lt;strike&gt;appstats/ui.py&lt;/strike&gt; appengine_config.py.&lt;br /&gt;&lt;span style="color: #990000;"&gt;Updated&lt;/span&gt;, thanks Guido!&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;import kay&lt;br /&gt;kay.setup_syspath()&lt;/pre&gt;&lt;br /&gt;That's all. Now I can use appstats with Kay framework. Here are some screenshots.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;b&gt;This is a dashboard.&lt;/b&gt;&lt;a href="http://1.bp.blogspot.com/_DkVkyz39PB0/SyLYirl2jtI/AAAAAAAADeo/OFxBzeB1NjA/s1600-h/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%EF%BC%882009-12-12+8.40.28%EF%BC%89.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/_DkVkyz39PB0/SyLYirl2jtI/AAAAAAAADeo/OFxBzeB1NjA/s640/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%EF%BC%882009-12-12+8.40.28%EF%BC%89.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;b&gt;This is a graphical timeline.&lt;/b&gt;&lt;a href="http://2.bp.blogspot.com/_DkVkyz39PB0/SyLYRJPlsgI/AAAAAAAADeg/5FiuJi3SpjQ/s1600-h/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%EF%BC%882009-12-12+8.39.08%EF%BC%89.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/_DkVkyz39PB0/SyLYRJPlsgI/AAAAAAAADeg/5FiuJi3SpjQ/s640/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%EF%BC%882009-12-12+8.39.08%EF%BC%89.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-4251928415648004419?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/4251928415648004419/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=4251928415648004419' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/4251928415648004419'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/4251928415648004419'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/12/using-appstats-with-kay.html' title='Using appstats with Kay'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_DkVkyz39PB0/SyLYirl2jtI/AAAAAAAADeo/OFxBzeB1NjA/s72-c/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%EF%BC%882009-12-12+8.40.28%EF%BC%89.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-3517054711573640956</id><published>2009-11-02T00:12:00.000+09:00</published><updated>2009-11-02T00:12:26.378+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='kay_fw'/><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>pkg_resources の warning を抑制する</title><content type='html'>sitecustomize.py を site-packages 内に下記の内容で作成する&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;import warnings&lt;br /&gt;warnings.filterwarnings('ignore',&lt;br /&gt;  message=r'Module .*? is being added to sys\.path', append=True)&lt;/pre&gt;&lt;br /&gt;ここに書いてありました:&lt;br /&gt;http://lucumr.pocoo.org/2008/2/19/sick-of-pkg-resources-warnings&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-3517054711573640956?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/3517054711573640956/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=3517054711573640956' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3517054711573640956'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3517054711573640956'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/11/pkgresources-warning.html' title='pkg_resources の warning を抑制する'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-3048829501785838952</id><published>2009-10-26T09:56:00.001+09:00</published><updated>2009-10-26T09:58:27.781+09:00</updated><title type='text'>More and more lazy loading...</title><content type='html'>I've spent a little while to make Kay load modules in more lazily manner, then I've got faster result on cold start, so I'd like to share the result with you.&lt;br /&gt;&lt;br /&gt;Basically, I've made this test in the same way as described &lt;a href="http://takashi-matsuo.blogspot.com/2009/10/minimum-cost-of-various-frameworks-cold.html"&gt;this entry&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h4&gt;Kay(changeset:4a854c31abbc)&lt;/h4&gt;&lt;pre&gt;10-25 05:33PM 31.791 / 200 344ms 544cpu_ms&lt;br /&gt;10-25 05:34PM 07.998 / 200 354ms 544cpu_ms&lt;br /&gt;10-25 05:34PM 37.308 / 200 355ms 563cpu_ms&lt;br /&gt;10-25 05:35PM 00.452 / 200 336ms 525cpu_ms&lt;br /&gt;10-25 05:35PM 27.638 / 200 332ms 525cpu_ms&lt;br /&gt;10-25 05:40PM 45.837 / 200 365ms 544cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Now Kay gets significant faster result! I'm planning to release Kay-0.3.0 soon.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-3048829501785838952?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/3048829501785838952/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=3048829501785838952' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3048829501785838952'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3048829501785838952'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/10/more-and-more-lazy-loading.html' title='More and more lazy loading...'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-826719584682190151</id><published>2009-10-21T02:45:00.011+09:00</published><updated>2009-10-21T08:09:21.591+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Kay'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='appengine'/><title type='text'>Minimum cost for warming-up various frameworks(and more)</title><content type='html'>&lt;h2&gt;Overview&lt;/h2&gt;&lt;br /&gt;There are lots of frameworks that works on appengine, so people might wonder which framework is the best one for them. So, I did a simple benchmark to see how much cpu time to warm-up various frameworks for appengine with minimized applications.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;How to test&lt;/h2&gt;The rule is quite simple.&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Use template system for rendering&lt;br /&gt;&lt;li&gt;Use very simple template&lt;br /&gt;&lt;li&gt;No middleware, context_processors&lt;br /&gt;&lt;li&gt;The view passes single string ('hello') to the template&lt;br /&gt;&lt;li&gt;No i18n (USE_I18N=False)&lt;br /&gt;&lt;/ul&gt;Today, I tested 3 frameworks; &lt;ul&gt;&lt;li&gt;webapp+django template&lt;br /&gt;&lt;li&gt;&lt;a href="http://code.google.com/p/kay-framework/"&gt;Kay framework&lt;/a&gt; repository tip (changeset: 6c871008872a)&lt;br /&gt;&lt;li&gt;&lt;a href="http://code.google.com/p/app-engine-patch/"&gt;app-engine-patch-1.1RC&lt;/a&gt;&lt;br /&gt;&lt;/ul&gt;The single series of tests was made as follows; &lt;ol&gt;&lt;li&gt;Upload an application&lt;br /&gt;&lt;li&gt;Access by browser once&lt;br /&gt;&lt;li&gt;View logs on admin console and record ms and cpu_ms values&lt;br /&gt;&lt;li&gt;Go to next framework in turn and do the same test&lt;br /&gt;&lt;/ol&gt;&lt;h2&gt;Results&lt;/h2&gt;And I did this series of tests five times and got following result: &lt;h3&gt;Cold Starting result&lt;/h3&gt;&lt;h4&gt;webapp+django template&lt;/h4&gt;&lt;pre&gt;10-20 09:29AM 32.796 / 200 332ms 369cpu_ms&lt;br /&gt;10-20 09:31AM 45.991 / 200 278ms 330cpu_ms&lt;br /&gt;10-20 09:33AM 56.086 / 200 352ms 369cpu_ms&lt;br /&gt;10-20 09:36AM 16.630 / 200 245ms 350cpu_ms&lt;br /&gt;10-20 09:39AM 13.148 / 200 266ms 350cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h4&gt;Kay&lt;/h4&gt;&lt;pre&gt;10-20 09:30AM 13.555 / 200 504ms 700cpu_ms&lt;br /&gt;10-20 09:32AM 27.076 / 200 436ms 641cpu_ms&lt;br /&gt;10-20 09:34AM 37.841 / 200 417ms 621cpu_ms&lt;br /&gt;10-20 09:37AM 01.469 / 200 471ms 641cpu_ms&lt;br /&gt;10-20 09:41AM 40.874 / 200 454ms 660cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h4&gt;app-engine-patch&lt;/h4&gt;&lt;pre&gt;10-20 09:31AM 00.640 / 200 663ms 1010cpu_ms&lt;br /&gt;10-20 09:33AM 12.117 / 200 594ms 991cpu_ms&lt;br /&gt;10-20 09:35AM 29.921 / 200 654ms 1030cpu_ms&lt;br /&gt;10-20 09:38AM 00.888 / 200 629ms 1030cpu_ms&lt;br /&gt;10-20 09:46AM 07.109 / 200 702ms 1108cpu_ms&lt;br /&gt;&lt;/pre&gt;Additionally, I did some tests for serving by hot instances:  &lt;h3&gt;Served by hot instances result&lt;/h3&gt;&lt;h4&gt;webapp+django template&lt;/h4&gt;&lt;pre&gt;10-20 09:39AM 23.025 / 200 6ms 3cpu_ms&lt;br /&gt;10-20 09:39AM 24.104 / 200 6ms 5cpu_ms&lt;br /&gt;10-20 09:39AM 25.212 / 200 9ms 5cpu_ms&lt;br /&gt;10-20 09:39AM 26.310 / 200 6ms 4cpu_ms&lt;br /&gt;10-20 09:39AM 27.414 / 200 7ms 4cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h4&gt;Kay&lt;/h4&gt;&lt;pre&gt;10-20 09:41AM 42.991 / 200 13ms 5cpu_ms&lt;br /&gt;10-20 09:41AM 45.691 / 200 6ms 5cpu_ms&lt;br /&gt;10-20 09:41AM 46.538 / 200 7ms 5cpu_ms&lt;br /&gt;10-20 09:41AM 47.506 / 200 9ms 8cpu_ms&lt;br /&gt;10-20 09:41AM 48.536 / 200 8ms 5cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h4&gt;app-engine-patch&lt;/h4&gt;&lt;pre&gt;10-20 09:46AM 09.021 / 200 10ms 10cpu_ms&lt;br /&gt;10-20 09:46AM 09.949 / 200 8ms 8cpu_ms&lt;br /&gt;10-20 09:46AM 10.869 / 200 8ms 6cpu_ms&lt;br /&gt;10-20 09:46AM 11.834 / 200 11ms 11cpu_ms&lt;br /&gt;10-20 09:46AM 12.743 / 200 9ms 10cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h2&gt;The application used in these tests&lt;/h2&gt;&lt;p&gt;The template used is common among these frameworks.&lt;/p&gt;index.html: &lt;pre class="prettyprint"&gt;&amp;lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"%gt;&lt;br /&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;head&amp;gt;&lt;br /&gt;&amp;lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&amp;gt;&lt;br /&gt;&amp;lt;title&amp;gt;Top Page - myapp&amp;lt;/title&amp;gt;&lt;br /&gt;&amp;lt;/head&amp;gt;&lt;br /&gt;&amp;lt;body&amp;gt;&lt;br /&gt;{{ message }}&lt;br /&gt;&amp;lt;/body&amp;gt;&lt;br /&gt;&amp;lt;/html&amp;gt;&lt;br /&gt;&lt;/pre&gt;Here are the application settings for each frameworks: &lt;h3&gt;webapp+django template&lt;/h3&gt;main.py: &lt;pre class="prettyprint"&gt;#!/usr/bin/env python&lt;br /&gt;#&lt;br /&gt;# Copyright 2007 Google Inc.&lt;br /&gt;#&lt;br /&gt;# Licensed under the Apache License, Version 2.0 (the "License");&lt;br /&gt;# you may not use this file except in compliance with the License.&lt;br /&gt;# You may obtain a copy of the License at&lt;br /&gt;#&lt;br /&gt;#     http://www.apache.org/licenses/LICENSE-2.0&lt;br /&gt;#&lt;br /&gt;# Unless required by applicable law or agreed to in writing, software&lt;br /&gt;# distributed under the License is distributed on an "AS IS" BASIS,&lt;br /&gt;# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;br /&gt;# See the License for the specific language governing permissions and&lt;br /&gt;# limitations under the License.&lt;br /&gt;#&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;import logging&lt;br /&gt;import os&lt;br /&gt;&lt;br /&gt;import wsgiref.handlers&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;from google.appengine.ext import webapp&lt;br /&gt;from google.appengine.ext import db&lt;br /&gt;from google.appengine.ext.webapp import template&lt;br /&gt;&lt;br /&gt;class MainHandler(webapp.RequestHandler):&lt;br /&gt;&lt;br /&gt;  def get(self):&lt;br /&gt;    contents = {'message': 'hello'}&lt;br /&gt;    path = os.path.join(os.path.dirname(__file__), 'index.html')&lt;br /&gt;    self.response.out.write(template.render(path, contents))&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;def main():&lt;br /&gt;  logging.getLogger().setLevel(logging.debug)&lt;br /&gt;  application = webapp.WSGIApplication([('/', MainHandler)],&lt;br /&gt;                                       debug=True)&lt;br /&gt;  wsgiref.handlers.CGIHandler().run(application)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;  main()&lt;br /&gt;&lt;/pre&gt;&lt;h3&gt;Kay&lt;/h3&gt;settings.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;&lt;br /&gt;"""&lt;br /&gt;A sample of kay settings.&lt;br /&gt;&lt;br /&gt;:Copyright: (c) 2009 Accense Technology, Inc. &lt;br /&gt;                     Takashi Matsuo &lt;tmatsuo@candit.jp&gt;,&lt;br /&gt;                     All rights reserved.&lt;br /&gt;:license: BSD, see LICENSE for more details.&lt;br /&gt;"""&lt;br /&gt;&lt;br /&gt;DEFAULT_TIMEZONE = 'Asia/Tokyo'&lt;br /&gt;DEBUG = False&lt;br /&gt;SECRET_KEY = 'ReplaceItWithSecretString'&lt;br /&gt;SESSION_PREFIX = 'gaesess:'&lt;br /&gt;COOKIE_AGE = 1209600 # 2 weeks&lt;br /&gt;COOKIE_NAME = 'KAY_SESSION'&lt;br /&gt;&lt;br /&gt;ADD_APP_PREFIX_TO_KIND = True&lt;br /&gt;&lt;br /&gt;ADMINS = (&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;TEMPLATE_DIRS = (&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;USE_I18N = False&lt;br /&gt;DEFAULT_LANG = 'en'&lt;br /&gt;&lt;br /&gt;INSTALLED_APPS = (&lt;br /&gt;  'myapp',&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;APP_MOUNT_POINTS = {&lt;br /&gt;  'myapp': '/',&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;CONTEXT_PROCESSORS = (&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;JINJA2_FILTERS = {&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;MIDDLEWARE_CLASSES = (&lt;br /&gt;)&lt;br /&gt;AUTH_USER_BACKEND = 'kay.auth.backend.GoogleBackend'&lt;br /&gt;AUTH_USER_MODEL = 'kay.auth.models.GoogleUser'&lt;br /&gt;&lt;/pre&gt;myapp/urls.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# myapp.urls&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;from werkzeug.routing import (&lt;br /&gt;  Map, Rule, Submount,&lt;br /&gt;  EndpointPrefix, RuleTemplate,&lt;br /&gt;)&lt;br /&gt;import myapp.views&lt;br /&gt;&lt;br /&gt;def make_rules():&lt;br /&gt;  return [&lt;br /&gt;    EndpointPrefix('myapp/', [&lt;br /&gt;      Rule('/', endpoint='index'),&lt;br /&gt;    ]),&lt;br /&gt;  ]&lt;br /&gt;&lt;br /&gt;all_views = {&lt;br /&gt;  'myapp/index': myapp.views.index,&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;myapp/views.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# myapp.views&lt;br /&gt;&lt;br /&gt;from kay.utils import render_to_response&lt;br /&gt;&lt;br /&gt;# Create your views here.&lt;br /&gt;&lt;br /&gt;def index(request):&lt;br /&gt;  return render_to_response('myapp/index.html', {'message': 'Hello'})&lt;br /&gt;&lt;/pre&gt;&lt;h3&gt;app-engine-patch&lt;/h3&gt;settings.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;from ragendja.settings_pre import *&lt;br /&gt;&lt;br /&gt;# Increase this when you update your media on the production site, so users&lt;br /&gt;# don't have to refresh their cache. By setting this your MEDIA_URL&lt;br /&gt;# automatically becomes /media/MEDIA_VERSION/&lt;br /&gt;MEDIA_VERSION = 1&lt;br /&gt;&lt;br /&gt;# By hosting media on a different domain we can get a speedup (more parallel&lt;br /&gt;# browser connections).&lt;br /&gt;#if on_production_server or not have_appserver:&lt;br /&gt;#    MEDIA_URL = 'http://media.mydomain.com/media/%d/'&lt;br /&gt;&lt;br /&gt;# Add base media (jquery can be easily added via INSTALLED_APPS)&lt;br /&gt;COMBINE_MEDIA = {&lt;br /&gt;    'combined-%(LANGUAGE_CODE)s.js': (&lt;br /&gt;        # See documentation why site_data can be useful:&lt;br /&gt;        # http://code.google.com/p/app-engine-patch/wiki/MediaGenerator&lt;br /&gt;        '.site_data.js',&lt;br /&gt;    ),&lt;br /&gt;    'combined-%(LANGUAGE_DIR)s.css': (&lt;br /&gt;        'global/look.css',&lt;br /&gt;    ),&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;# Change your email settings&lt;br /&gt;if on_production_server:&lt;br /&gt;    DEFAULT_FROM_EMAIL = 'bla@bla.com'&lt;br /&gt;    SERVER_EMAIL = DEFAULT_FROM_EMAIL&lt;br /&gt;&lt;br /&gt;# Make this unique, and don't share it with anybody.&lt;br /&gt;SECRET_KEY = '1234567890'&lt;br /&gt;&lt;br /&gt;#ENABLE_PROFILER = True&lt;br /&gt;#ONLY_FORCED_PROFILE = True&lt;br /&gt;#PROFILE_PERCENTAGE = 25&lt;br /&gt;#SORT_PROFILE_RESULTS_BY = 'cumulative' # default is 'time'&lt;br /&gt;# Profile only datastore calls&lt;br /&gt;#PROFILE_PATTERN = 'ext.db..+\((?:get|get_by_key_name|fetch|count|put)\)'&lt;br /&gt;&lt;br /&gt;# Enable I18N and set default language to 'en'&lt;br /&gt;USE_I18N = False&lt;br /&gt;LANGUAGE_CODE = 'en'&lt;br /&gt;&lt;br /&gt;# Restrict supported languages (and JS media generation)&lt;br /&gt;LANGUAGES = (&lt;br /&gt;    ('de', 'German'),&lt;br /&gt;    ('en', 'English'),&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;TEMPLATE_CONTEXT_PROCESSORS = (&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;MIDDLEWARE_CLASSES = (&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;# Google authentication&lt;br /&gt;AUTH_USER_MODULE = 'ragendja.auth.google_models'&lt;br /&gt;AUTH_ADMIN_MODULE = 'ragendja.auth.google_admin'&lt;br /&gt;# Hybrid Django/Google authentication&lt;br /&gt;#AUTH_USER_MODULE = 'ragendja.auth.hybrid_models'&lt;br /&gt;&lt;br /&gt;LOGIN_URL = '/account/login/'&lt;br /&gt;LOGOUT_URL = '/account/logout/'&lt;br /&gt;LOGIN_REDIRECT_URL = '/'&lt;br /&gt;&lt;br /&gt;INSTALLED_APPS = (&lt;br /&gt;    # Add jquery support (app is in "common" folder). This automatically&lt;br /&gt;    # adds jquery to your COMBINE_MEDIA['combined-%(LANGUAGE_CODE)s.js']&lt;br /&gt;    # Note: the order of your INSTALLED_APPS specifies the order in which&lt;br /&gt;    # your app-specific media files get combined, so jquery should normally&lt;br /&gt;    # come first.&lt;br /&gt;    'appenginepatcher',&lt;br /&gt;    'ragendja',&lt;br /&gt;    'myapp',&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;# List apps which should be left out from app settings and urlsauto loading&lt;br /&gt;IGNORE_APP_SETTINGS = IGNORE_APP_URLSAUTO = (&lt;br /&gt;    # Example:&lt;br /&gt;    # 'django.contrib.admin',&lt;br /&gt;    # 'django.contrib.auth',&lt;br /&gt;    # 'yetanotherapp',&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;# Remote access to production server (e.g., via manage.py shell --remote)&lt;br /&gt;DATABASE_OPTIONS = {&lt;br /&gt;    # Override remoteapi handler's path (default: '/remote_api').&lt;br /&gt;    # This is a good idea, so you make it not too easy for hackers. ;)&lt;br /&gt;    # Don't forget to also update your app.yaml!&lt;br /&gt;    #'remote_url': '/remote-secret-url',&lt;br /&gt;&lt;br /&gt;    # !!!Normally, the following settings should not be used!!!&lt;br /&gt;&lt;br /&gt;    # Always use remoteapi (no need to add manage.py --remote option)&lt;br /&gt;    #'use_remote': True,&lt;br /&gt;&lt;br /&gt;    # Change appid for remote connection (by default it's the same as in&lt;br /&gt;    # your app.yaml)&lt;br /&gt;    #'remote_id': 'otherappid',&lt;br /&gt;&lt;br /&gt;    # Change domain (default: &lt;remoteid&gt;.appspot.com)&lt;br /&gt;    #'remote_host': 'bla.com',&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;from ragendja.settings_post import *&lt;br /&gt;&lt;/pre&gt;urls.py(minimized): &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;from django.conf.urls.defaults import *&lt;br /&gt;from ragendja.urlsauto import urlpatterns&lt;br /&gt;from ragendja.auth.urls import urlpatterns as auth_patterns&lt;br /&gt;&lt;br /&gt;urlpatterns = auth_patterns + patterns('',&lt;br /&gt;    (r'', include('myapp.urls')),&lt;br /&gt;) + urlpatterns&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;myapp/urls.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;from django.conf.urls.defaults import *&lt;br /&gt;&lt;br /&gt;urlpatterns = patterns(&lt;br /&gt;  'myapp.views',&lt;br /&gt;  url(r'^$', 'index', name='myapp_index'),&lt;br /&gt;)&lt;br /&gt;&lt;/pre&gt;myapp/views.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;from ragendja.template import render_to_response&lt;br /&gt;&lt;br /&gt;def index(request):&lt;br /&gt;  return render_to_response(request, 'myapp/index.html',&lt;br /&gt;                            {'message': 'Hello'})&lt;br /&gt;&lt;/pre&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;There are significant defferences among warm-up costs of these three frameworks. But when it comes to serving by warm instances, the defferences are rather small. &lt;p&gt;Actually you can choose whatever you want, but what if you must to choose one framework among these three options? &lt;p&gt;webapp+django template is really fast(actually without django template, webapp is much faster than this, but I think its unfair ;-P), so If you really need speed, webapp might be the best option. &lt;p&gt;Having said that, sometimes we need a fancy way for constructing rather complex applications. Perhaps you can choose Kay/app-engine-patch if you can accept 2 or 3 times(obviously compared with webapp) costs on cold-startup. &lt;p&gt;If you love Django admin capability and hardly throw it away, only app-engine-patch could be your option. &lt;p&gt;Actually I don't know much about app-engine-patch, so these settings is not enough. Please let me know if that's the case. If you make some tests similar to these tests, I'd be glad to know the result.  &lt;div style="color: red"&gt;The result with &lt;a href="http://code.google.com/p/tipfy/"&gt;tipfy&lt;/a&gt; is added.&lt;/div&gt;&lt;a href="http://code.google.com/p/tipfy/"&gt;tipfy&lt;/a&gt; is one of the fastest and lightest frameworks. You can use a very nice debugger(werkzeug's) in dev environment(This debugger is also available with Kay).&lt;h3&gt;Cold Starting result&lt;/h3&gt;&lt;h4&gt;tipfy&lt;/h4&gt;&lt;pre&gt;10-20 03:43PM 28.814 / 200 411ms 602cpu_ms&lt;br /&gt;10-20 03:44PM 31.751 / 200 367ms 563cpu_ms&lt;br /&gt;10-20 03:45PM 09.913 / 200 407ms 563cpu_ms&lt;br /&gt;10-20 03:46PM 23.932 / 200 413ms 602cpu_ms&lt;br /&gt;10-20 03:47PM 19.530 / 200 416ms 583cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h3&gt;Served by hot instances result&lt;/h3&gt;&lt;h4&gt;tipfy&lt;/h4&gt;&lt;pre&gt;10-20 03:47PM 21.859 / 200 6ms 4cpu_ms&lt;br /&gt;10-20 03:47PM 23.003 / 200 8ms 5cpu_ms&lt;br /&gt;10-20 03:47PM 24.177 / 200 6ms 4cpu_ms&lt;br /&gt;10-20 03:47PM 25.166 / 200 6ms 4cpu_ms&lt;br /&gt;10-20 03:47PM 26.181 / 200 5ms 4cpu_ms&lt;br /&gt;&lt;/pre&gt;&lt;h3&gt;Application&lt;/h3&gt;&lt;h4&gt;tipfy&lt;/h4&gt;urls.py: &lt;pre class="prettyprint"&gt;# -*- coding: utf-8 -*-&lt;br /&gt;"""&lt;br /&gt;    urls&lt;br /&gt;    ~~~~&lt;br /&gt;&lt;br /&gt;    URL definitions.&lt;br /&gt;&lt;br /&gt;    :copyright: 2009 by tipfy.org.&lt;br /&gt;    :license: BSD, see LICENSE.txt for more details.&lt;br /&gt;"""&lt;br /&gt;from tipfy import Rule&lt;br /&gt;&lt;br /&gt;urls = [&lt;br /&gt;    Rule('/', endpoint='home', handler='hello:HelloWorldHandler'),&lt;br /&gt;]&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;hello.py: &lt;pre class="prettyprint"&gt;from tipfy import RequestHandler&lt;br /&gt;from tipfy.ext.jinja2 import render_response&lt;br /&gt;&lt;br /&gt;class HelloWorldHandler(RequestHandler):&lt;br /&gt;  def get(self, **kwargs):&lt;br /&gt;    context = {'message': 'hello'}&lt;br /&gt;    return render_response('index.html', **context)&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-826719584682190151?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/826719584682190151/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=826719584682190151' title='16 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/826719584682190151'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/826719584682190151'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/10/minimum-cost-of-various-frameworks-cold.html' title='Minimum cost for warming-up various frameworks(and more)'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>16</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-5481667747552079455</id><published>2009-08-31T22:30:00.002+09:00</published><updated>2009-08-31T22:38:42.342+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Kay'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='pyjamas'/><title type='text'>Using pyjamas on Kay</title><content type='html'>I use ${PYJAMAS_HOME} and ${PROJECT_DIR} variable for explanation here. ${PYJAMAS_HOME} is a directory in which you installed pyjamas. ${PROJECT_DIR} is your project directory.&lt;br /&gt;&lt;br /&gt;For opener, please begin with customizing ${PYJAMAS_HOME}/examples/jsonrpc. You need only JSONRPCExample.html in the `public` directory.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ tree kay-pyjamas-sample&lt;br /&gt;kay-pyjamas-sample&lt;br /&gt;|-- JSONRPCExample.py&lt;br /&gt;`-- public&lt;br /&gt;    `-- JSONRPCExample.html&lt;br /&gt;&lt;br /&gt;1 directory, 2 files&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Edit your own JSONRPCExample.py as follows:&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;import pyjd # dummy in pyjs&lt;br /&gt;&lt;br /&gt;from pyjamas.ui.RootPanel import RootPanel&lt;br /&gt;from pyjamas.ui.TextArea import TextArea&lt;br /&gt;from pyjamas.ui.Label import Label&lt;br /&gt;from pyjamas.ui.Button import Button&lt;br /&gt;from pyjamas.ui.HTML import HTML&lt;br /&gt;from pyjamas.ui.VerticalPanel import VerticalPanel&lt;br /&gt;from pyjamas.ui.HorizontalPanel import HorizontalPanel&lt;br /&gt;from pyjamas.ui.ListBox import ListBox&lt;br /&gt;from pyjamas.JSONService import JSONProxy&lt;br /&gt;&lt;br /&gt;class JSONRPCExample:&lt;br /&gt;    def onModuleLoad(self):&lt;br /&gt;        self.TEXT_WAITING = "Waiting for response..."&lt;br /&gt;        self.TEXT_ERROR = "Server Error"&lt;br /&gt;        self.METHOD_ECHO = "Echo"&lt;br /&gt;        self.METHOD_REVERSE = "Reverse"&lt;br /&gt;        self.METHOD_UPPERCASE = "UPPERCASE"&lt;br /&gt;        self.METHOD_LOWERCASE = "lowercase"&lt;br /&gt;        self.methods = [self.METHOD_ECHO, self.METHOD_REVERSE, self.METHOD_UPPERCASE, self.METHOD_LOWERCASE]&lt;br /&gt;&lt;br /&gt;        self.remote_py = EchoServicePython()&lt;br /&gt;&lt;br /&gt;        self.status=Label()&lt;br /&gt;        self.text_area = TextArea()&lt;br /&gt;        self.text_area.setText("""{'Test'} [\"String\"]&lt;br /&gt;\tTest Tab&lt;br /&gt;Test Newline\n&lt;br /&gt;after newline&lt;br /&gt;""" + r"""Literal String:&lt;br /&gt;{'Test'} [\"String\"]&lt;br /&gt;""")&lt;br /&gt;        self.text_area.setCharacterWidth(80)&lt;br /&gt;        self.text_area.setVisibleLines(8)&lt;br /&gt;        &lt;br /&gt;        self.method_list = ListBox()&lt;br /&gt;        self.method_list.setName("hello")&lt;br /&gt;        self.method_list.setVisibleItemCount(1)&lt;br /&gt;        for method in self.methods:&lt;br /&gt;            self.method_list.addItem(method)&lt;br /&gt;        self.method_list.setSelectedIndex(0)&lt;br /&gt;&lt;br /&gt;        method_panel = HorizontalPanel()&lt;br /&gt;        method_panel.add(HTML("Remote string method to call: "))&lt;br /&gt;        method_panel.add(self.method_list)&lt;br /&gt;        method_panel.setSpacing(8)&lt;br /&gt;&lt;br /&gt;        self.button_py = Button("Send to Python Service", self)&lt;br /&gt;&lt;br /&gt;        buttons = HorizontalPanel()&lt;br /&gt;        buttons.add(self.button_py)&lt;br /&gt;        buttons.setSpacing(8)&lt;br /&gt;        &lt;br /&gt;        info = """&lt;h2&gt;JSON-RPC Example&lt;/h2&gt;&lt;br /&gt;        &lt;p&gt;This example demonstrates the calling of server services with&lt;br /&gt;           &lt;a href="http://json-rpc.org/"&gt;JSON-RPC&lt;/a&gt;.&lt;br /&gt;        &lt;/p&gt;&lt;br /&gt;        &lt;p&gt;Enter some text below, and press a button to send the text&lt;br /&gt;           to an Echo service on your server. An echo service simply sends the exact same text back that it receives.&lt;br /&gt;           &lt;/p&gt;"""&lt;br /&gt;        &lt;br /&gt;        panel = VerticalPanel()&lt;br /&gt;        panel.add(HTML(info))&lt;br /&gt;        panel.add(self.text_area)&lt;br /&gt;        panel.add(method_panel)&lt;br /&gt;        panel.add(buttons)&lt;br /&gt;        panel.add(self.status)&lt;br /&gt;        &lt;br /&gt;        RootPanel().add(panel)&lt;br /&gt;&lt;br /&gt;    def onClick(self, sender):&lt;br /&gt;        self.status.setText(self.TEXT_WAITING)&lt;br /&gt;        method = self.methods[self.method_list.getSelectedIndex()]&lt;br /&gt;        text = self.text_area.getText()&lt;br /&gt;        id = -1&lt;br /&gt;        if method == self.METHOD_ECHO:&lt;br /&gt;            id = self.remote_py.echo(text, self)&lt;br /&gt;        elif method == self.METHOD_REVERSE:&lt;br /&gt;            id = self.remote_py.reverse(text, self)&lt;br /&gt;        elif method == self.METHOD_UPPERCASE:&lt;br /&gt;            id = self.remote_py.uppercase(text, self)&lt;br /&gt;        elif method == self.METHOD_LOWERCASE:&lt;br /&gt;            id = self.remote_py.lowercase(text, self)&lt;br /&gt;        if id&lt;0:&lt;br /&gt;            self.status.setText(self.TEXT_ERROR)&lt;br /&gt;&lt;br /&gt;    def onRemoteResponse(self, response, request_info):&lt;br /&gt;        self.status.setText(response)&lt;br /&gt;&lt;br /&gt;    def onRemoteError(self, code, message, request_info):&lt;br /&gt;        self.status.setText("Server Error or Invalid Response: ERROR %d - %s" %&lt;br /&gt;                            (code, message))&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;class EchoServicePython(JSONProxy):&lt;br /&gt;    def __init__(self):&lt;br /&gt;        JSONProxy.__init__(self, "/json", ["echo", "reverse", "uppercase", "lowercase"])&lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;    # for pyjd, set up a web server and load the HTML from there:&lt;br /&gt;    # this convinces the browser engine that the AJAX will be loaded&lt;br /&gt;    # from the same URI base as the URL, it's all a bit messy...&lt;br /&gt;    pyjd.setup("http://127.0.0.1/examples/jsonrpc/public/JSONRPCExample.html")&lt;br /&gt;    app = JSONRPCExample()&lt;br /&gt;    app.onModuleLoad()&lt;br /&gt;    pyjd.run()&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Then, cd into this directory and compile it. This time, I use ${PROJECT_DIR}/media for a target directory.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ cd kay-pyjamas-sample&lt;br /&gt;$ ${PYJAMAS_HOME}/bin/pyjsbuild -o ${PROJECT_DIR}/media JSONRPCExample.py&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;If succeed, you'll get ${PROJECT_DIR}/media/JSONRPCExample.html. Please access http://localhost:8080/media/JSONRPCExample.html and, you'll get forms for testing jsonrpc. &lt;br /&gt;&lt;br /&gt;Let's write server side code. First, you need to edit urls.py in your application.&lt;br /&gt;&lt;br /&gt;${PROJECT_DIR}/myapp/urls.py&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# myapp.urls&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;from werkzeug.routing import (&lt;br /&gt;  Map, Rule, Submount,&lt;br /&gt;  EndpointPrefix, RuleTemplate,&lt;br /&gt;)&lt;br /&gt;import myapp.views&lt;br /&gt;&lt;br /&gt;def make_rules():&lt;br /&gt;  return [&lt;br /&gt;    EndpointPrefix('myapp/', [&lt;br /&gt;      Rule('/', endpoint='index'),&lt;br /&gt;      Rule('/json', endpoint='json_rpc'),&lt;br /&gt;    ]),&lt;br /&gt;  ]&lt;br /&gt;&lt;br /&gt;all_views = {&lt;br /&gt;  'myapp/index': myapp.views.index,&lt;br /&gt;  'myapp/json_rpc': myapp.views.json_rpc,&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;You also need to edit views.py.&lt;br /&gt;&lt;br /&gt;${PROJECT_DIR}/myapp/views.py&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# myapp.views&lt;br /&gt;# ... (snip)&lt;br /&gt;&lt;br /&gt;import simplejson&lt;br /&gt;&lt;br /&gt;# ... (snip)&lt;br /&gt;&lt;br /&gt;def json_uppercase(args):&lt;br /&gt;  return [args[0].upper()]&lt;br /&gt;&lt;br /&gt;def json_echo(args):&lt;br /&gt;  return [args[0]]&lt;br /&gt;&lt;br /&gt;def json_reverse(args):&lt;br /&gt;  return [args[0][::-1]]&lt;br /&gt;&lt;br /&gt;def json_lowercase(args):&lt;br /&gt;  return [args[0].lower()]&lt;br /&gt;&lt;br /&gt;def json_rpc(request):&lt;br /&gt;  if request.method:&lt;br /&gt;    args = simplejson.loads(request.data)&lt;br /&gt;    method_name = 'json_%s' % args[u"method"]&lt;br /&gt;    json_func = globals().get(method_name)&lt;br /&gt;    json_params = args[u"params"]&lt;br /&gt;    json_method_id = args[u"id"]&lt;br /&gt;    result = json_func(json_params)&lt;br /&gt;    args.pop(u"method")&lt;br /&gt;    args["result"] = result[0]&lt;br /&gt;    args["error"] = None&lt;br /&gt;    return Response(simplejson.dumps(args), content_type="application/json")&lt;br /&gt;  else:&lt;br /&gt;    return Response('Error')&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;That's all. Please enjoy :-)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-5481667747552079455?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/5481667747552079455/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=5481667747552079455' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5481667747552079455'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5481667747552079455'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/08/using-pyjamas-on-kay.html' title='Using pyjamas on Kay'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-2830262589254310174</id><published>2009-08-31T22:04:00.002+09:00</published><updated>2009-08-31T22:23:50.797+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Kay'/><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='pyjamas'/><title type='text'>Kay で pyjamas を使う</title><content type='html'>&lt;a href="http://kay-docs-jp.shehas.net/"&gt;日本語の情報もできた&lt;/a&gt;のでちょっと変わり種をやります。&lt;br /&gt;&lt;br /&gt;Python 版 GWT である &lt;a href="http://pyjs.org/"&gt;pyjamas&lt;/a&gt; を Kay と組み合わせてみました.. と言っても jsonrpc の簡単なサンプルを動かしただけですが。&lt;br /&gt;&lt;br /&gt;Pyjamas をインストールした場所を ${PYJAMAS_HOME} と、プロジェクトのディレクトリを ${PROJECT_DIR} として進めましょう。&lt;br /&gt;&lt;br /&gt;${PYJAMAS_HOME}/examples/jsonrpc をいじって使います。public 以下は JSONRPCExample.html だけ必要みたい。&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ tree kay-pyjamas-sample&lt;br /&gt;kay-pyjamas-sample&lt;br /&gt;|-- JSONRPCExample.py&lt;br /&gt;`-- public&lt;br /&gt;    `-- JSONRPCExample.html&lt;br /&gt;&lt;br /&gt;1 directory, 2 files&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;JSONRPCExample.py を下記のように少しいじります。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;import pyjd # dummy in pyjs&lt;br /&gt;&lt;br /&gt;from pyjamas.ui.RootPanel import RootPanel&lt;br /&gt;from pyjamas.ui.TextArea import TextArea&lt;br /&gt;from pyjamas.ui.Label import Label&lt;br /&gt;from pyjamas.ui.Button import Button&lt;br /&gt;from pyjamas.ui.HTML import HTML&lt;br /&gt;from pyjamas.ui.VerticalPanel import VerticalPanel&lt;br /&gt;from pyjamas.ui.HorizontalPanel import HorizontalPanel&lt;br /&gt;from pyjamas.ui.ListBox import ListBox&lt;br /&gt;from pyjamas.JSONService import JSONProxy&lt;br /&gt;&lt;br /&gt;class JSONRPCExample:&lt;br /&gt;    def onModuleLoad(self):&lt;br /&gt;        self.TEXT_WAITING = "Waiting for response..."&lt;br /&gt;        self.TEXT_ERROR = "Server Error"&lt;br /&gt;        self.METHOD_ECHO = "Echo"&lt;br /&gt;        self.METHOD_REVERSE = "Reverse"&lt;br /&gt;        self.METHOD_UPPERCASE = "UPPERCASE"&lt;br /&gt;        self.METHOD_LOWERCASE = "lowercase"&lt;br /&gt;        self.methods = [self.METHOD_ECHO, self.METHOD_REVERSE, self.METHOD_UPPERCASE, self.METHOD_LOWERCASE]&lt;br /&gt;&lt;br /&gt;        self.remote_py = EchoServicePython()&lt;br /&gt;&lt;br /&gt;        self.status=Label()&lt;br /&gt;        self.text_area = TextArea()&lt;br /&gt;        self.text_area.setText("""{'Test'} [\"String\"]&lt;br /&gt;\tTest Tab&lt;br /&gt;Test Newline\n&lt;br /&gt;after newline&lt;br /&gt;""" + r"""Literal String:&lt;br /&gt;{'Test'} [\"String\"]&lt;br /&gt;""")&lt;br /&gt;        self.text_area.setCharacterWidth(80)&lt;br /&gt;        self.text_area.setVisibleLines(8)&lt;br /&gt;        &lt;br /&gt;        self.method_list = ListBox()&lt;br /&gt;        self.method_list.setName("hello")&lt;br /&gt;        self.method_list.setVisibleItemCount(1)&lt;br /&gt;        for method in self.methods:&lt;br /&gt;            self.method_list.addItem(method)&lt;br /&gt;        self.method_list.setSelectedIndex(0)&lt;br /&gt;&lt;br /&gt;        method_panel = HorizontalPanel()&lt;br /&gt;        method_panel.add(HTML("Remote string method to call: "))&lt;br /&gt;        method_panel.add(self.method_list)&lt;br /&gt;        method_panel.setSpacing(8)&lt;br /&gt;&lt;br /&gt;        self.button_py = Button("Send to Python Service", self)&lt;br /&gt;&lt;br /&gt;        buttons = HorizontalPanel()&lt;br /&gt;        buttons.add(self.button_py)&lt;br /&gt;        buttons.setSpacing(8)&lt;br /&gt;        &lt;br /&gt;        info = """&lt;h2&gt;JSON-RPC Example&lt;/h2&gt;&lt;br /&gt;        &lt;p&gt;This example demonstrates the calling of server services with&lt;br /&gt;           &lt;a href="http://json-rpc.org/"&gt;JSON-RPC&lt;/a&gt;.&lt;br /&gt;        &lt;/p&gt;&lt;br /&gt;        &lt;p&gt;Enter some text below, and press a button to send the text&lt;br /&gt;           to an Echo service on your server. An echo service simply sends the exact same text back that it receives.&lt;br /&gt;           &lt;/p&gt;"""&lt;br /&gt;        &lt;br /&gt;        panel = VerticalPanel()&lt;br /&gt;        panel.add(HTML(info))&lt;br /&gt;        panel.add(self.text_area)&lt;br /&gt;        panel.add(method_panel)&lt;br /&gt;        panel.add(buttons)&lt;br /&gt;        panel.add(self.status)&lt;br /&gt;        &lt;br /&gt;        RootPanel().add(panel)&lt;br /&gt;&lt;br /&gt;    def onClick(self, sender):&lt;br /&gt;        self.status.setText(self.TEXT_WAITING)&lt;br /&gt;        method = self.methods[self.method_list.getSelectedIndex()]&lt;br /&gt;        text = self.text_area.getText()&lt;br /&gt;        id = -1&lt;br /&gt;        if method == self.METHOD_ECHO:&lt;br /&gt;            id = self.remote_py.echo(text, self)&lt;br /&gt;        elif method == self.METHOD_REVERSE:&lt;br /&gt;            id = self.remote_py.reverse(text, self)&lt;br /&gt;        elif method == self.METHOD_UPPERCASE:&lt;br /&gt;            id = self.remote_py.uppercase(text, self)&lt;br /&gt;        elif method == self.METHOD_LOWERCASE:&lt;br /&gt;            id = self.remote_py.lowercase(text, self)&lt;br /&gt;        if id&lt;0:&lt;br /&gt;            self.status.setText(self.TEXT_ERROR)&lt;br /&gt;&lt;br /&gt;    def onRemoteResponse(self, response, request_info):&lt;br /&gt;        self.status.setText(response)&lt;br /&gt;&lt;br /&gt;    def onRemoteError(self, code, message, request_info):&lt;br /&gt;        self.status.setText("Server Error or Invalid Response: ERROR %d - %s" %&lt;br /&gt;                            (code, message))&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;class EchoServicePython(JSONProxy):&lt;br /&gt;    def __init__(self):&lt;br /&gt;        JSONProxy.__init__(self, "/json", ["echo", "reverse", "uppercase", "lowercase"])&lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;    # for pyjd, set up a web server and load the HTML from there:&lt;br /&gt;    # this convinces the browser engine that the AJAX will be loaded&lt;br /&gt;    # from the same URI base as the URL, it's all a bit messy...&lt;br /&gt;    pyjd.setup("http://127.0.0.1/examples/jsonrpc/public/JSONRPCExample.html")&lt;br /&gt;    app = JSONRPCExample()&lt;br /&gt;    app.onModuleLoad()&lt;br /&gt;    pyjd.run()&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;そしたらそのディレクトリに入ってコンパイルします。今回はターゲットを直&lt;br /&gt;接 ${PROJECT_DIR}/media にしてしまいます。&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ cd kay-pyjamas-sample&lt;br /&gt;$ ${PYJAMAS_HOME}/bin/pyjsbuild -o ${PROJECT_DIR}/media JSONRPCExample.py&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;成功すると ${PROJECT_DIR}/media/JSONRPCExample.html が出来ます。&lt;br /&gt;http://localhost:8080/media/JSONRPCExample.html で画面が出ていればまあ成功です。&lt;br /&gt;&lt;br /&gt;サーバーサイドは下記の通りにします。myapp はアプリケーション名に読み換&lt;br /&gt;えてくださいね。&lt;br /&gt;&lt;br /&gt;${PROJECT_DIR}/myapp/urls.py&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# myapp.urls&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;from werkzeug.routing import (&lt;br /&gt;  Map, Rule, Submount,&lt;br /&gt;  EndpointPrefix, RuleTemplate,&lt;br /&gt;)&lt;br /&gt;import myapp.views&lt;br /&gt;&lt;br /&gt;def make_rules():&lt;br /&gt;  return [&lt;br /&gt;    EndpointPrefix('myapp/', [&lt;br /&gt;      Rule('/', endpoint='index'),&lt;br /&gt;      Rule('/json', endpoint='json_rpc'),&lt;br /&gt;    ]),&lt;br /&gt;  ]&lt;br /&gt;&lt;br /&gt;all_views = {&lt;br /&gt;  'myapp/index': myapp.views.index,&lt;br /&gt;  'myapp/json_rpc': myapp.views.json_rpc,&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;最後に view です。&lt;br /&gt;&lt;br /&gt;${PROJECT_DIR}/myapp/views.py&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;# -*- coding: utf-8 -*-&lt;br /&gt;# myapp.views&lt;br /&gt;# ... 省略&lt;br /&gt;&lt;br /&gt;import simplejson&lt;br /&gt;&lt;br /&gt;# ... 省略&lt;br /&gt;&lt;br /&gt;def json_uppercase(args):&lt;br /&gt;  return [args[0].upper()]&lt;br /&gt;&lt;br /&gt;def json_echo(args):&lt;br /&gt;  return [args[0]]&lt;br /&gt;&lt;br /&gt;def json_reverse(args):&lt;br /&gt;  return [args[0][::-1]]&lt;br /&gt;&lt;br /&gt;def json_lowercase(args):&lt;br /&gt;  return [args[0].lower()]&lt;br /&gt;&lt;br /&gt;def json_rpc(request):&lt;br /&gt;  if request.method:&lt;br /&gt;    args = simplejson.loads(request.data)&lt;br /&gt;    method_name = 'json_%s' % args[u"method"]&lt;br /&gt;    json_func = globals().get(method_name)&lt;br /&gt;    json_params = args[u"params"]&lt;br /&gt;    json_method_id = args[u"id"]&lt;br /&gt;    result = json_func(json_params)&lt;br /&gt;    args.pop(u"method")&lt;br /&gt;    args["result"] = result[0]&lt;br /&gt;    args["error"] = None&lt;br /&gt;    return Response(simplejson.dumps(args), content_type="application/json")&lt;br /&gt;  else:&lt;br /&gt;    return Response('Error')&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;適当な作りですが、まあこれで動きます。興味があれば色々工夫してみてくだ&lt;br /&gt;さい。&lt;br /&gt;&lt;br /&gt;今日はここでおしまいです。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-2830262589254310174?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/2830262589254310174/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=2830262589254310174' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/2830262589254310174'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/2830262589254310174'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/08/kay-pyjamas.html' title='Kay で pyjamas を使う'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-7915371480304873712</id><published>2009-08-04T14:24:00.010+09:00</published><updated>2009-08-04T15:10:13.047+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='Kay'/><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>Kay framework を使ってみる</title><content type='html'>&lt;a href="http://code.google.com/p/kay-framework/"&gt;Kay framework&lt;/a&gt; を 7/7 にリリースしたわけですが、日本語での情報が無いので少しずつ書いていくことにしようと思います。&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;始め方&lt;/h2&gt;&lt;br /&gt;必要なものは下記の通りです。&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;Python-2.5&lt;br /&gt;&lt;li&gt;App Engine SDK Python&lt;br /&gt;&lt;li&gt;Kay framework&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;br /&gt;macports を使って python25 を入れた場合は、他に下記もインストールしましょう。&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;py25-hashlib&lt;br /&gt;&lt;li&gt;py25-socket-ssl&lt;br /&gt;&lt;li&gt;py25-pil&lt;br /&gt;&lt;li&gt;py25-ipython&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;br /&gt;Kay のリリース版を使う場合は、下記のダウンロードページから tar ball を落として使います。リポジトリを追いかける場合は、mercurial で clone してください。&lt;br /&gt;&lt;br /&gt;ダウンロードページ: &lt;a href="http://code.google.com/p/kay-framework/downloads/list"&gt;http://code.google.com/p/kay-framework/downloads/list&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;clone するには:&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ hg clone https://kay-framework.googlecode.com/hg/ kay&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;プロジェクトを始める&lt;/h2&gt;&lt;br /&gt;kay の manage.py スクリプトでプロジェクトディレクトリを作る事ができます。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ python kay/manage.py startproject myproject&lt;br /&gt;$ tree myproject&lt;br /&gt;myproject&lt;br /&gt;|-- app.yaml&lt;br /&gt;|-- kay -&gt; /Users/tmatsuo/work/tmp/kay/kay&lt;br /&gt;|-- manage.py -&gt; /Users/tmatsuo/work/tmp/kay/manage.py&lt;br /&gt;|-- settings.py&lt;br /&gt;`-- urls.py&lt;br /&gt;&lt;br /&gt;1 directory, 4 files&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;シンボリックリンクをサポートしているプラットフォームでは kay ディレクトリと manage.py へのシンボリックリンクが作成されます。後で kay の場所を動かすときっと動かなくなるのですが、そんな時はリンクを張り直してください。&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;アプリケーションを作る&lt;/h2&gt;&lt;br /&gt;出来たばかりの myproject ディレクトリに cd して、早速アプリケーションを作りましょう。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ cd myproject&lt;br /&gt;$ python manage.py startapp myapp&lt;br /&gt;$ tree myapp&lt;br /&gt;myapp&lt;br /&gt;|-- __init__.py&lt;br /&gt;|-- models.py&lt;br /&gt;|-- templates&lt;br /&gt;|   `-- index.html&lt;br /&gt;|-- urls.py&lt;br /&gt;`-- views.py&lt;br /&gt;&lt;br /&gt;1 directory, 5 files&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;こうして作成した myapp を動作させるにはもうひと手間必要です。settings.py の INSTALLED_APPS に登録します。必要なら APP_MOUNT_POINTS も登録します。下記の例では、アプリケーションをルート URL にマウントする例です。APP_MOUNT_POINTS を設定しない場合は、/myapp というようにアプリケーション名 URL にマウントされます。&lt;br /&gt;&lt;br /&gt;settings.py&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;#$/usr/bin/python&lt;br /&gt;#..&lt;br /&gt;#..&lt;br /&gt;&lt;br /&gt;INSTALLED_APPS = (&lt;br /&gt;  'kay.sessions',&lt;br /&gt;  'myapp',&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;APP_MOUNT_POINTS = {&lt;br /&gt;  'myapp': '/',&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;見れば分かると思いますが、INSTALLED_APPS はタプルで、APP_MOUNT_POINTS は dict になっています。&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;アプリケーションを動かす&lt;/h2&gt;&lt;br /&gt;作ったアプリケーションを動かしてみましょう。下記のコマンドで開発サーバが起動する筈です。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ python manage.py runserver&lt;br /&gt;INFO     2009-08-04 05:48:21,339 appengine_rpc.py:157] Server: appengine.google.com&lt;br /&gt;...&lt;br /&gt;...&lt;br /&gt;INFO     2009-08-04 05:48:21,448 dev_appserver_main.py:465] Running application myproject on port 8080: http://localhost:8080&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;この状態で http://localhost:8080/ にアクセスすると、「Hello」又は「こんにちは」と表示される筈です。&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;GAE にアップロードする&lt;/h2&gt;&lt;br /&gt;GAE にアップロードするには、対象の appid を app.yaml の application に設定してから、下記のコマンドを使用します。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ python manage.py appcfg update&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;成功すると、http://your-appid.appspot.com/ でアクセスできるようになります。&lt;br /&gt;&lt;br /&gt;今日はここまでにしますね。けっこう簡単に始められる事がお分かりいただけたでしょうか。次回はモデル定義とか、フォームの自動生成などについてお話しする予定です。&lt;br /&gt;&lt;br /&gt;当分このシリーズを続けるつもりなので、取り上げて欲しいトピックなどがあればコメントください。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-7915371480304873712?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/7915371480304873712/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=7915371480304873712' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7915371480304873712'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7915371480304873712'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/08/kay-framework.html' title='Kay framework を使ってみる'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-5604170700462838617</id><published>2009-05-15T00:45:00.003+09:00</published><updated>2009-05-15T10:02:54.633+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='Werkzeug'/><title type='text'>A patch for Werkzeug debugger on GAE</title><content type='html'>I wrote about &lt;a href="http://takashi-matsuo.blogspot.com/2009/05/using-werkzeugs-debugger-on-gae-dev.html"&gt;using werkzeug on gae&lt;/a&gt;, but actually there are still several problems in debug console. So I wrote a patch for it.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;&lt;br /&gt;diff -r 8ffe4e637e17 werkzeug/debug/console.py&lt;br /&gt;--- a/werkzeug/debug/console.py Sat Apr 25 17:06:40 2009 +0000&lt;br /&gt;+++ b/werkzeug/debug/console.py Fri May 15 00:40:32 2009 +0900&lt;br /&gt;@@ -32,6 +32,12 @@&lt;br /&gt;     def close(self):&lt;br /&gt;         pass&lt;br /&gt; &lt;br /&gt;+    def flush(self):&lt;br /&gt;+        pass&lt;br /&gt;+&lt;br /&gt;+    def seek(self, n, mode=0):&lt;br /&gt;+        pass&lt;br /&gt;+&lt;br /&gt;     def reset(self):&lt;br /&gt;         val = ''.join(self._buffer)&lt;br /&gt;         del self._buffer[:]&lt;br /&gt;@@ -48,12 +54,18 @@&lt;br /&gt;     def writelines(self, x):&lt;br /&gt;         self._write(escape(''.join(x)))&lt;br /&gt; &lt;br /&gt;+    def readline(self):&lt;br /&gt;+        if len(self._buffer) == 0:&lt;br /&gt;+            return ''&lt;br /&gt;+        ret = self._buffer[0]&lt;br /&gt;+        del self._buffer[0]&lt;br /&gt;+        return ret&lt;br /&gt; &lt;br /&gt; class ThreadedStream(object):&lt;br /&gt;     """Thread-local wrapper for sys.stdout for the interactive console."""&lt;br /&gt; &lt;br /&gt;     def push():&lt;br /&gt;-        if sys.stdout is sys.__stdout__:&lt;br /&gt;+        if not isinstance(sys.stdout, ThreadedStream):&lt;br /&gt;             sys.stdout = ThreadedStream()&lt;br /&gt;         _local.stream = HTMLStringO()&lt;br /&gt;     push = staticmethod(push)&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-5604170700462838617?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/5604170700462838617/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=5604170700462838617' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5604170700462838617'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5604170700462838617'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/05/patch-for-werkzeug-debugger-on-gae.html' title='A patch for Werkzeug debugger on GAE'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6688256995023808648</id><published>2009-05-14T20:13:00.004+09:00</published><updated>2009-05-14T20:27:47.693+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='Werkzeug'/><title type='text'>Using Werkzeug's debugger on GAE dev server</title><content type='html'>There is a very good web framework called Werkzeug. The debugger of Werkzeug is pretty nice and we can use inline console on our web browser with it.&lt;br /&gt;&lt;br /&gt;The author of Werkzeug wrote &lt;a href="http://dev.pocoo.org/projects/werkzeug/wiki/UsingDebuggerWithAppEngine"&gt;an article &lt;/a&gt; of how to use this debugger with GAE dev server. However according this article, the inline console is not usable.&lt;br /&gt;&lt;br /&gt;After a little struggling, finally I can use the debugger on GAE dev server. GAE dev server creates an instance of DebuggedApplication on every request. I think its the reason why. So, it seems that specifying the instance as the module global singleton made it work.&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;&lt;br /&gt;_debugged_app = None&lt;br /&gt;app = ... build your wsgi app ...&lt;br /&gt;&lt;br /&gt;def main():&lt;br /&gt;  # Only run the debugger in development.&lt;br /&gt;  import os, sys&lt;br /&gt;  if 'SERVER_SOFTWARE' in os.environ and os.environ['SERVER_SOFTWARE'].startswith('Dev'):&lt;br /&gt;    # use our debug.utils with Jinja2 templates&lt;br /&gt;    import debug.utils&lt;br /&gt;    sys.modules['werkzeug.debug.utils'] = debug.utils&lt;br /&gt;&lt;br /&gt;    # don't use inspect.getsourcefile because the imp module is empty &lt;br /&gt;    import inspect&lt;br /&gt;    inspect.getsourcefile = inspect.getfile&lt;br /&gt;    &lt;br /&gt;    # wrap the application&lt;br /&gt;    from werkzeug import DebuggedApplication&lt;br /&gt;    global _debugged_app&lt;br /&gt;    if _debugged_app is None:&lt;br /&gt;      _debugged_app = app = DebuggedApplication(app, evalex=True)&lt;br /&gt;    else:&lt;br /&gt;      app = _debugged_app    &lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6688256995023808648?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6688256995023808648/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6688256995023808648' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6688256995023808648'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6688256995023808648'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/05/using-werkzeugs-debugger-on-gae-dev.html' title='Using Werkzeug&apos;s debugger on GAE dev server'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6499636094223859814</id><published>2009-05-14T19:55:00.006+09:00</published><updated>2009-05-14T20:29:27.296+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='Werkzeug'/><title type='text'>Google App Engine で Werkzeug のデバッガを使う</title><content type='html'>Werkzeug という Python Web Framework があります。Werkzeug のデバッガはなかなかすぐれもので Web アプリでエラーが出るとインライン console が使えたりします。&lt;br /&gt;&lt;br /&gt;このデバッガを GAE の dev server で使う&lt;a href="http://dev.pocoo.org/projects/werkzeug/wiki/UsingDebuggerWithAppEngine"&gt;方法&lt;/a&gt;を作者の方が書いているのですが、この通りやるとインライン console が使えなかったりします。&lt;br /&gt;&lt;br /&gt;ちょっと調べてみたら意外と簡単に使えました。GAE の dev server だとリクエスト毎に DebuggedApplication のインスタンスが作り直されてしまうので、これを module global のシングルトンにしてやればオッケイでした。&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;&lt;br /&gt;_debugged_app = None&lt;br /&gt;app = ... build your wsgi app ...&lt;br /&gt;&lt;br /&gt;def main():&lt;br /&gt;  # Only run the debugger in development.&lt;br /&gt;  import os, sys&lt;br /&gt;  if 'SERVER_SOFTWARE' in os.environ and os.environ['SERVER_SOFTWARE'].startswith('Dev'):&lt;br /&gt;    # use our debug.utils with Jinja2 templates&lt;br /&gt;    import debug.utils&lt;br /&gt;    sys.modules['werkzeug.debug.utils'] = debug.utils&lt;br /&gt;&lt;br /&gt;    # don't use inspect.getsourcefile because the imp module is empty &lt;br /&gt;    import inspect&lt;br /&gt;    inspect.getsourcefile = inspect.getfile&lt;br /&gt;    &lt;br /&gt;    # wrap the application&lt;br /&gt;    from werkzeug import DebuggedApplication&lt;br /&gt;    global _debugged_app&lt;br /&gt;    if _debugged_app is None:&lt;br /&gt;      _debugged_app = app = DebuggedApplication(app, evalex=True)&lt;br /&gt;    else:&lt;br /&gt;      app = _debugged_app&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6499636094223859814?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6499636094223859814/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6499636094223859814' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6499636094223859814'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6499636094223859814'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/05/google-app-engine-werkzeug.html' title='Google App Engine で Werkzeug のデバッガを使う'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-3793562541976869269</id><published>2009-03-20T17:42:00.007+09:00</published><updated>2009-03-22T15:06:06.078+09:00</updated><title type='text'>Android hack-a-thon</title><content type='html'>3/20に &lt;a href="http://googlejapan.blogspot.com/2009/03/android-hackathon.html"&gt;Android hack-a-thon&lt;/a&gt; に参加してきました。&lt;br /&gt;&lt;br /&gt;みなさん一日で色々作っています。すごいな〜&lt;br /&gt;&lt;br /&gt;私の方は、全くの初心者なので午前中は HelloWorld をやってみました。午後からは gdata のライブラリを使って Google Apps のグループアドレスを作成できるようなアプリケーションにトライ。&lt;br /&gt;&lt;br /&gt;gdata の jar をプロジェクトに取り込み、必要な画面を作るところまではうまくいったけど、 GroupService の認証をするところでうまくいかず、当日はここで断念。原因はSSL証明書の検証が失敗している事まではつきとめたんですけど...&lt;br /&gt;&lt;br /&gt;せっかくなので今日がんばって動かす事ができたので書きます。&lt;br /&gt;&lt;br /&gt;まず gdata の Java クライアントのソースを落としてきて、下記のパッチを当てます。内容はSSL証明書の検証が必ず成功するようにしているだけです。&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;diff -uNr gdata/java/src/com/google/gdata/client/GoogleAuthTokenFactory.java gdata.mine/java/src/com/google/gdata/client/GoogleAuthTokenFactory.java&lt;br /&gt;--- gdata/java/src/com/google/gdata/client/GoogleAuthTokenFactory.java 2009-02-12 05:09:34.000000000 +0900&lt;br /&gt;+++ gdata.mine/java/src/com/google/gdata/client/GoogleAuthTokenFactory.java 2009-03-22 13:49:42.000000000 +0900&lt;br /&gt;@@ -16,6 +16,13 @@&lt;br /&gt;&lt;br /&gt;package com.google.gdata.client;&lt;br /&gt;&lt;br /&gt;+import org.apache.http.conn.ssl.X509HostnameVerifier;&lt;br /&gt;+import javax.security.cert.X509Certificate;&lt;br /&gt;+import javax.net.ssl.HttpsURLConnection;&lt;br /&gt;+import javax.net.ssl.SSLSession;&lt;br /&gt;+import javax.net.ssl.SSLSocket;&lt;br /&gt;+import javax.net.ssl.SSLException;&lt;br /&gt;+&lt;br /&gt;import com.google.gdata.util.common.base.CharEscapers;&lt;br /&gt;import com.google.gdata.util.common.base.StringUtil;&lt;br /&gt;import com.google.gdata.client.GoogleService.AccountDeletedException;&lt;br /&gt;@@ -450,7 +457,27 @@&lt;br /&gt;&lt;br /&gt;    // Open connection&lt;br /&gt;    HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();&lt;br /&gt;-&lt;br /&gt;+    if ( urlConnection instanceof HttpsURLConnection ) {&lt;br /&gt;+      HttpsURLConnection ucs = (HttpsURLConnection) urlConnection;&lt;br /&gt;+      ucs.setHostnameVerifier( new X509HostnameVerifier(){&lt;br /&gt;+   @Override&lt;br /&gt;+     public boolean verify(String arg0, SSLSession arg1) {&lt;br /&gt;+     return true;&lt;br /&gt;+   }&lt;br /&gt;+   @Override&lt;br /&gt;+     public void verify(String arg0, SSLSocket arg1) throws IOException {&lt;br /&gt;+   }&lt;br /&gt;+   @Override&lt;br /&gt;+     public void verify(String arg0,&lt;br /&gt;+          java.security.cert.X509Certificate arg1)&lt;br /&gt;+     throws SSLException {&lt;br /&gt;+   }&lt;br /&gt;+   @Override&lt;br /&gt;+     public void verify(String arg0, String[] arg1, String[]&lt;br /&gt;+          arg2) throws SSLException {&lt;br /&gt;+   }&lt;br /&gt;+        });&lt;br /&gt;+    }&lt;br /&gt;    // Set properties of the connection&lt;br /&gt;    urlConnection.setDoInput(true);&lt;br /&gt;    urlConnection.setDoOutput(true);&lt;br /&gt;diff -uNr gdata/java/src/com/google/gdata/client/appsforyourdomain/AppsGroupsService.java gdata.mine/java/src/com/google/gdata/client/appsforyourdomain/AppsGroupsService.java&lt;br /&gt;--- gdata/java/src/com/google/gdata/client/appsforyourdomain/AppsGroupsService.java 2009-02-12 05:09:34.000000000 +0900&lt;br /&gt;+++ gdata.mine/java/src/com/google/gdata/client/appsforyourdomain/AppsGroupsService.java 2009-03-22 14:10:47.000000000 +0900&lt;br /&gt;@@ -37,7 +37,7 @@&lt;br /&gt;public class AppsGroupsService extends AppsPropertyService {&lt;br /&gt;&lt;br /&gt;  public static final String BASE_URL =&lt;br /&gt;-      "http://apps-apis.google.com/a/feeds/group/2.0/";&lt;br /&gt;+      "https://apps-apis.google.com/a/feeds/group/2.0/";&lt;br /&gt;  public final String baseDomainUrl;&lt;br /&gt;&lt;br /&gt;  public static final String APPS_PROP_GROUP_ID = "groupId";&lt;br /&gt;diff -uNr gdata/java/src/com/google/gdata/client/http/HttpGDataRequest.java gdata.mine/java/src/com/google/gdata/client/http/HttpGDataRequest.java&lt;br /&gt;--- gdata/java/src/com/google/gdata/client/http/HttpGDataRequest.java 2009-02-12 05:09:35.000000000 +0900&lt;br /&gt;+++ gdata.mine/java/src/com/google/gdata/client/http/HttpGDataRequest.java 2009-03-22 13:49:39.000000000 +0900&lt;br /&gt;@@ -16,6 +16,13 @@&lt;br /&gt;&lt;br /&gt;package com.google.gdata.client.http;&lt;br /&gt;&lt;br /&gt;+import org.apache.http.conn.ssl.X509HostnameVerifier;&lt;br /&gt;+import javax.security.cert.X509Certificate;&lt;br /&gt;+import javax.net.ssl.HttpsURLConnection;&lt;br /&gt;+import javax.net.ssl.SSLSession;&lt;br /&gt;+import javax.net.ssl.SSLSocket;&lt;br /&gt;+import javax.net.ssl.SSLException;&lt;br /&gt;+&lt;br /&gt;import com.google.gdata.util.common.xml.XmlWriter;&lt;br /&gt;import com.google.gdata.client.AuthTokenFactory;&lt;br /&gt;import com.google.gdata.client.GDataProtocol;&lt;br /&gt;@@ -312,6 +319,29 @@&lt;br /&gt;    }&lt;br /&gt;    HttpURLConnection uc = (HttpURLConnection) requestUrl.openConnection();&lt;br /&gt;&lt;br /&gt;+    // Open connection&lt;br /&gt;+    if ( uc instanceof HttpsURLConnection ) {&lt;br /&gt;+      HttpsURLConnection ucs = (HttpsURLConnection) uc;&lt;br /&gt;+      ucs.setHostnameVerifier( new X509HostnameVerifier(){&lt;br /&gt;+            @Override&lt;br /&gt;+       public boolean verify(String arg0, SSLSession arg1) {&lt;br /&gt;+       return true;&lt;br /&gt;+            }&lt;br /&gt;+            @Override&lt;br /&gt;+       public void verify(String arg0, SSLSocket arg1) throws IOException {&lt;br /&gt;+            }&lt;br /&gt;+            @Override&lt;br /&gt;+       public void verify(String arg0,&lt;br /&gt;+     java.security.cert.X509Certificate arg1)&lt;br /&gt;+       throws SSLException {&lt;br /&gt;+            }&lt;br /&gt;+            @Override&lt;br /&gt;+       public void verify(String arg0, String[] arg1, String[]&lt;br /&gt;+     arg2) throws SSLException {&lt;br /&gt;+            }&lt;br /&gt;+        });&lt;br /&gt;+    }&lt;br /&gt;+&lt;br /&gt;    // Should never cache GData requests/responses&lt;br /&gt;    uc.setUseCaches(false);&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_DkVkyz39PB0/ScXUQlDnPKI/AAAAAAAACo0/jTjREf2EKiE/s1600-h/android-myfirstapp.png"&gt;&lt;img style="margin: 0pt 0pt 10px 10px; float: right; cursor: pointer; width: 320px; height: 266px;" src="http://1.bp.blogspot.com/_DkVkyz39PB0/ScXUQlDnPKI/AAAAAAAACo0/jTjREf2EKiE/s320/android-myfirstapp.png" alt="" id="BLOGGER_PHOTO_ID_5315888316471000226" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;あと gdata/java/build-src/core.xml のコンパイル箇所で android.jar に classpath を通せばコンパイルが通ります。(メッセージに従って mail.jar と activation.jar を用意する必要もあるけど)&lt;br /&gt;&lt;br /&gt;でき上がった gdata-appsforyourdomain-1.0.jar と gdata-core-1.0.jar を使うときちんと動かすことができました。手軽に Google Apps の Group を追加できるだけのしょーもないアプリですが...&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;これを進化させて Google Apps の管理コンソールが作れそう。でも需要は無さそうだなorz&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-3793562541976869269?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/3793562541976869269/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=3793562541976869269' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3793562541976869269'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3793562541976869269'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/03/android-hack-thon.html' title='Android hack-a-thon'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_DkVkyz39PB0/ScXUQlDnPKI/AAAAAAAACo0/jTjREf2EKiE/s72-c/android-myfirstapp.png' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-3318536078775738549</id><published>2009-03-05T08:09:00.004+09:00</published><updated>2009-03-05T08:20:47.540+09:00</updated><title type='text'>Werkzeug の easteregg</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_DkVkyz39PB0/Sa8MOF_8K0I/AAAAAAAACH4/6_FH1JBLwL8/s1600-h/werkzeug-easteregg.png"&gt;&lt;img style="margin: 0pt 0pt 10px 10px; float: right; cursor: pointer; width: 211px; height: 282px;" src="http://2.bp.blogspot.com/_DkVkyz39PB0/Sa8MOF_8K0I/AAAAAAAACH4/6_FH1JBLwL8/s320/werkzeug-easteregg.png" alt="" id="BLOGGER_PHOTO_ID_5309475921961167682" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;僕も最近話題の Werkzeug を使ってみようと思いました。しかしまだ使うまでにいたらず、とりあえず easteregg を発見して喜んでいる状態です。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-3318536078775738549?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/3318536078775738549/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=3318536078775738549' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3318536078775738549'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3318536078775738549'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/03/werkzeug-easteregg.html' title='Werkzeug の easteregg'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_DkVkyz39PB0/Sa8MOF_8K0I/AAAAAAAACH4/6_FH1JBLwL8/s72-c/werkzeug-easteregg.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6349235878516959283</id><published>2009-02-23T02:25:00.018+09:00</published><updated>2009-02-26T13:05:44.013+09:00</updated><title type='text'>Gaebarを使ってみた</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_DkVkyz39PB0/SaYTLl2Ek4I/AAAAAAAACHs/GPThUWQmE8o/s1600-h/gaebar.png"&gt;&lt;img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 320px; height: 207px;" src="http://3.bp.blogspot.com/_DkVkyz39PB0/SaYTLl2Ek4I/AAAAAAAACHs/GPThUWQmE8o/s320/gaebar.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5306950300761101186" /&gt;&lt;/a&gt;&lt;br /&gt;先日 &lt;a href="http://app-gallery.appspot.com/"&gt;App Gallery&lt;/a&gt; という GAE 上のサービスを始めてみました。サービスをやるからにはデータをバックアップしたいと思い、&lt;a href="http://aralbalkan.com/1784"&gt;Gaebar&lt;/a&gt; を試してみたのでご紹介します。&lt;br /&gt;&lt;br /&gt;なんと Gaebar の画面は django pony です!&lt;br /&gt;&lt;br /&gt;Gaebar は Google App Engine Backup And Restore の略で、文字通り Google App Engine 用のバックアップ・リストアツールです。ゲイバーと読むそうです。この名前が良いですねw&lt;br /&gt;&lt;br /&gt;Gaebar は Django のアプリケーションで簡単に使えます。GAE上で動く既存の Django アプリケーションにプラグインする事で、バックアップ・リストア機能を追加する事ができます。&lt;br /&gt;&lt;br /&gt;残念ながら今は Django で動いているアプリケーションでしか使えませんが、少し頑張れば同じ事は別のフレームワークでも可能だと思います。&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight:bold;"&gt;準備する&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;まずは Gaebar をダウンロードします。&lt;br /&gt;&lt;a href="http://github.com/aral/gaebar/tree/master"&gt;Gaebar github&lt;/a&gt; から Download 機能を使ってアーカイブをダウンロードするか、下記のように git で clone しても良いです。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ git clone git://github.com/aral/gaebar.git&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;またはあなたのプロジェクトが git を使っているなら下記のコマンドが良いかもしれません。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;$ git submodule add git://github.com/aral/gaebar.git &lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;方法はともあれ、プロジェクト直下に gaebar というディレクトリができていれば大丈夫です。&lt;br /&gt;&lt;br /&gt;次に GAE の SDK にパッチを当てます。これは Gaebar がデータを python のファイルとしてダウンロードしてきてローカルのファイルシステムに保存するために必要なのです。方法が &lt;a href="http://aralbalkan.com/1440"&gt;AralのBlog Entry&lt;/a&gt; に乗っていますが、この情報は少し古いので、SDK-1.1.9では下記のパッチを使ってください。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;--- ../google_appengine/google/appengine/tools/dev_appserver.py.org 2009-02-26 11:33:33.000000000 +0900&lt;br /&gt;+++ ../google_appengine/google/appengine/tools/dev_appserver.py 2009-02-20 17:04:46.000000000 +0900&lt;br /&gt;@@ -1274,6 +1274,7 @@&lt;br /&gt;       allowed_symbols = self._WHITE_LIST_PARTIAL_MODULES[module.__name__]&lt;br /&gt;       for symbol in set(module.__dict__) - set(allowed_symbols):&lt;br /&gt;         if not (symbol.startswith('__') and symbol.endswith('__')):&lt;br /&gt;+          module.__dict__['old_'+symbol] = module.__dict__[symbol]&lt;br /&gt;           del module.__dict__[symbol]&lt;br /&gt; &lt;br /&gt;     if module.__name__ in self._MODULE_OVERRIDES:&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;注意) app-engine-patch を使う場合は、settings.py で DJANGO_STYLE_MODEL_KIND = False にしないと動きませんでした。(少なくとも私は動かせませんでした。)動かせた方がいたら教えてください :-)&lt;br /&gt;&lt;br /&gt;1. settings.py は下記のようにします。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;  INSTALLED_APPS = (&lt;br /&gt;    # Other apps...&lt;br /&gt;    'gaebar',&lt;br /&gt;  )&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;2. urls.py は下記のとおりです&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;urlpatterns = patterns('',&lt;br /&gt;&lt;br /&gt;  # ...other URLs&lt;br /&gt;&lt;br /&gt;  url(r'^gaebar/', include('gaebar.urls')),&lt;br /&gt;)&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;3. app.yaml に static folder を追加します。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;# Static: Gaebar&lt;br /&gt;- url: /gaebar/static&lt;br /&gt;  static_dir: gaebar/static&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;4. index.yaml にインデックスを追加します。一回 dev server で Gaebar を動かせば自動で追加されます。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;- kind: GaebarCodeShard&lt;br /&gt;  properties:&lt;br /&gt;  - name: backup&lt;br /&gt;  - name: created_at&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;5. Gaebar の設定を settings.py に追加します。&lt;br /&gt;&lt;br /&gt;GAEBAR_LOCAL_URL: ローカル開発サーバの絶対 URL を書きます。&lt;br /&gt;GAEBAR_SECRET_KEY: 秘密の文字列を記入します。誰にも秘密にしてください。&lt;br /&gt;GAEBAR_SERVERS: dict でサーバーを列挙します。&lt;br /&gt;GAEBAR_MODELS: tuple でバックアップするモデルを列挙します。&lt;br /&gt;&lt;br /&gt;App Gallery で使用している設定のサンプルを下記に掲載します。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;GAEBAR_LOCAL_URL = 'http://localhost:8000'&lt;br /&gt;GAEBAR_SECRET_KEY = 'replace_with_your_secret_key'&lt;br /&gt;&lt;br /&gt;GAEBAR_SERVERS = {&lt;br /&gt;  u'Deployment': u'http://app-gallery.appspot.com',&lt;br /&gt;  u'Staging': u'http://app-gallery-staging.appspot.com',&lt;br /&gt;  u'Local Test': u'http://localhost:8080',&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt;GAEBAR_MODELS = (&lt;br /&gt;  (&lt;br /&gt;    'appgallery.models',&lt;br /&gt;    (u'Application', u'ThumbnailImage'),&lt;br /&gt;  ),&lt;br /&gt;  (&lt;br /&gt;    'tags.models',&lt;br /&gt;    (u'Tag', u'TagApplication')&lt;br /&gt;  ),&lt;br /&gt;  (&lt;br /&gt;    'favorites.models',&lt;br /&gt;    (u'Favorite',)&lt;br /&gt;  ),&lt;br /&gt;  (&lt;br /&gt;    'ragendja.auth.custom_models',&lt;br /&gt;    (u'User',),&lt;br /&gt;  ),&lt;br /&gt;)&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;あと注意点がもう一つ。ローカル開発サーバーで django.contrib.csrf.middleware.CsrfMiddleware を使っているとバックアップがうまく行きませんでした。なので私はローカル開発サーバーでだけ CsrfMiddleware をオフにしています。&lt;br /&gt;&lt;br /&gt;やった！これで Gaebar が使用できます。使う前に、Gaebar がどんな風に動くか説明しておきますね。&lt;br /&gt;&lt;br /&gt;Gaebar は datastore のデータを Python code としてバックアップします。その Python code を実行する事でリストアします。バックアップは長くかかりますが、GAE はそれを許さないので、Gaebar は バックアップとリストアのプロセスを細切れにして ajax を使って繰り返し呼び出します。&lt;br /&gt;&lt;br /&gt;デフォルトでは Gaebar は 5 つの Entity をバックアップし Python code に変換し、1MB の制限を回避するために 300KB くらいのコード断片として datastore に保存します。バックアップの過程では、このすぐ後に、コード断片をローカルサーバー側にダウンロードして保存する事になります。&lt;br /&gt;&lt;br /&gt;もしあなたがもっと高い Quota を持っているなら、views.py をいじる事でこれらのデフォルト値を変えられます。&lt;br /&gt;&lt;br /&gt;Gaebar を使う時は、十分にテストしてから使ってくださいね。万が一データが消失しても、私や Aral は責任を持てませんので。&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight:bold;"&gt;使い方&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;A. リモートバックアップを取るには&lt;br /&gt;&lt;br /&gt;1. Gaebar をあなたのアプリケーションと一緒に GAE にデプロイします&lt;br /&gt;&lt;br /&gt;注意: デプロイする時に、gaebar/backups フォルダ内のバックアップデータも一緒にアップロードされる事を頭にいれておいてください。不必要ならバックアップデータは別の場所に移動した状態でデプロイすると良いでしょう。&lt;br /&gt;&lt;br /&gt;2. 本番サーバで Gaebar にアクセスします。(例: http://myapp.appspot.com/gaebar/ )&lt;br /&gt;&lt;br /&gt;3. Create New Backup ボタンをクリックします。&lt;br /&gt;&lt;br /&gt;* バックアップをする時に、ローカル開発サーバーが動作している必要があります。&lt;br /&gt;&lt;br /&gt;注意: reference error が発生した場合(存在しない Entity への ReferenceProperty)は、バックアップ時には無視されます。リストアすれば reference error は消滅します。&lt;br /&gt;&lt;br /&gt;注意: 今までバックアップした一番大きなデータストアは 18995 行で 223 code 断片、35MB ありました。ローカル開発サーバーへのリストアは一晩中かかりました(ローカル開発サーバーの datastore は激遅なので)。もし記録を破ったら aral@aralbalkan.com に教えてあげてください :-)&lt;br /&gt;&lt;br /&gt;注意2: バックアップ中の例外を詳しく見たい場合は、本番サーバーの settings.py で DEBUG_PROPAGATE_EXCEPTIONS = True と設定してください。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;B. ローカルでリストアする&lt;br /&gt;&lt;br /&gt;開発中に、リアル世界のデータを使ってテストするために、ローカルの開発サーバーにデータをリストアできます。&lt;br /&gt;&lt;br /&gt;1. ローカル開発サーバの Gaebar ページに行きます。(例: http://localhost:8000/gaebar/ )&lt;br /&gt;2. リストからリストアしたいバックアップデータを選びます。&lt;br /&gt;3. 終わるまで待ちます。&lt;br /&gt;&lt;br /&gt;tips: あらかじめ /var/tmp/ にある datastore の history ファイルのバックアップを取った方が良いです。&lt;br /&gt;&lt;br /&gt;ローカルのデータストアは非常に遅いので、覚悟してください。&lt;br /&gt;&lt;br /&gt;C. ステージング環境にリストアする&lt;br /&gt;&lt;br /&gt;GAE にはバージョニング機能がありますが、datastore は別バージョン間でも共通です。ですからステージング用の別アプリを使用するのは良い考えでしょう。&lt;br /&gt;&lt;br /&gt;本番とは別のアプリケーションスロットを用意して、そこにリストアしたいバックアップデータと一緒に Gaebar をデプロイします。後はローカル開発サーバーでのリストアと同じです。&lt;br /&gt;&lt;br /&gt;D. 本番環境にリストアする&lt;br /&gt;&lt;br /&gt;本番環境でトラブルがあった場合などは、バックアップからリストアできます。データストアのデータは上書きされるので注意してください。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;実際に App Gallery でもバックアップ・リストアしてみましたが、完璧に動いています。Gaebar はかなり便利なのでみなさんも使ってみてはいかがでしょうか。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6349235878516959283?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6349235878516959283/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6349235878516959283' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6349235878516959283'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6349235878516959283'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/02/gaebar.html' title='Gaebarを使ってみた'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_DkVkyz39PB0/SaYTLl2Ek4I/AAAAAAAACHs/GPThUWQmE8o/s72-c/gaebar.png' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6719569197005072391</id><published>2009-02-11T03:15:00.009+09:00</published><updated>2009-02-11T03:41:57.374+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='mercurial'/><title type='text'>Mercurial deps extension</title><content type='html'>Mercurial で svn:externals みたいな事できないのか #mercurial irc で聞いてみたところ、&lt;a href="http://www.selenic.com/mercurial/wiki/index.cgi/DepsExtension"&gt;deps extension&lt;/a&gt;を使うと良いらしい。&lt;br /&gt;&lt;br /&gt;Mercurial に同梱されてないので別途&lt;a href="http://hg.hacks-galore.org/aleix/hgdeps"&gt;このへん&lt;/a&gt;から落として来て使う。&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;設定&lt;/h2&gt;&lt;br /&gt;deps.py を適当なところに置いて、hgrc に下記のように設定する。&lt;br /&gt;&lt;pre class="prettyprint"&gt;[extensions]&lt;br /&gt;hgext.deps =&lt;br /&gt;# 又は deps.py を hgext dir 以外に置いたなら下記のようにする&lt;br /&gt;# deps = /path/to/deps.py&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;ロケーション設定&lt;/h2&gt;&lt;br /&gt;依存関係のロケーションを設定する。これも hgrc に書く。ここで紹介する例は libfoo が外部 Mercurial リポジトリにあって、libbar は外部の CVS で管理されている場合。&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;[deps]&lt;br /&gt;aliases = libfoo, libbar&lt;br /&gt;alias.libfoo = /path/to/libfoo&lt;br /&gt;alias.libbar = :pserver:anonymous@cvs.server.org:/sources/bar&lt;br /&gt;alias.libbar.command = cvs -z3 -d$source co -r $rev -d $dest bar&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;実際の依存関係を設定&lt;/h2&gt;&lt;br /&gt;これはコマンドで行う。&lt;br /&gt;&lt;pre class="prettyprint"&gt;$ hg deps -a libfoo -r 3a9b061bada1 -d lib/foo 0.9.1&lt;br /&gt;$ hg deps -a libbar -r v0r7_8 -d lib/bar 0.9.1&lt;br /&gt;&lt;br /&gt;$ hg deps -a libfoo -r f24139319bdb -d lib/foo 1.0&lt;br /&gt;$ hg deps -a libbar -r v0r8_0 -d lib/bar 1.0&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;書式はこう&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;hg deps -a &lt;ロケーション設定のalias名&gt; -r &lt;外部のリビジョン&gt; -d &lt;work dir内での場所&gt; &lt;本体のバージョン&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;本体のバージョンに応じて外部リポジトリのリビジョンを変えたりできるわけか。&lt;br /&gt;&lt;br /&gt;で、これをすると下記のような .hgdeps というファイルが出来る。これも hg add するのが良いみたい。&lt;br /&gt;&lt;pre class="prettyprint"&gt;[1.0]&lt;br /&gt;f24139319bdb    libfoo    lib/foo&lt;br /&gt;v0r8_0          libbar    lib/bar&lt;br /&gt;&lt;br /&gt;[0.9.1]&lt;br /&gt;3a9b061bada1    libfoo    lib/foo&lt;br /&gt;v0r7_8          libbar    lib/bar&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;依存関係を持ってくる&lt;/h2&gt;&lt;br /&gt;&lt;br /&gt;下記のようにして clone するくらいしか機能は無いみたい。&lt;br /&gt;&lt;pre class="prettyprint"&gt;$ hg depsclone 1.0&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;この例では、バージョン 1.0 用の外部依存関係を取ってくる事ができる。&lt;br /&gt;&lt;br /&gt;まあすごく便利っていうわけでも無いし、使いたい人も限られてるだろうけど、Mercurial で svn:externals みたいな事がしたい人は試してみてください。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6719569197005072391?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6719569197005072391/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6719569197005072391' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6719569197005072391'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6719569197005072391'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/02/mercurial-deps-extension.html' title='Mercurial deps extension'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-9148447690423870576</id><published>2009-01-31T10:43:00.003+09:00</published><updated>2009-01-31T10:53:15.412+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Google apps'/><title type='text'>Google Apps に Group 機能が来ました</title><content type='html'>いつの間にか Google Apps の有料版にグループ機能が来ました。もしかするとコントロールパネルを拡張版にしないと出てこないかもしれません。おそらく今までの maillist 機能を拡張した機能で、ざっと触ってみたところ&lt;br /&gt;&lt;br /&gt;&lt;ol&gt;&lt;br /&gt;&lt;li&gt;いくらでも入れ子に出来る&lt;/li&gt;&lt;br /&gt;&lt;li&gt;メンバーのロールが Owner と Member の二種類ある&lt;/li&gt;&lt;br /&gt;&lt;li&gt;ドメイン外のユーザを Owner としても Member としても追加できる&lt;/li&gt;&lt;br /&gt;&lt;li&gt;このグループへの送信者を下記のグループ単位で制限できる&lt;/li&gt;&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;Owners&lt;br /&gt;&lt;li&gt;Members&lt;br /&gt;&lt;li&gt;ドメイン内のユーザ&lt;br /&gt;&lt;li&gt;Any mail address&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;li&gt;どうやら Google Document や Google Sites の invite の時に使えそう&lt;br /&gt;&lt;/ol&gt;&lt;br /&gt;&lt;br /&gt;今までは Google Apps を導入するお客さんには別立てでメーリングリストのサーバ(mailmanとか)を勧める事が多かったけど、これからはあまり必要なくなるかもしれませんね。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-9148447690423870576?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/9148447690423870576/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=9148447690423870576' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/9148447690423870576'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/9148447690423870576'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/01/google-apps-group.html' title='Google Apps に Group 機能が来ました'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6425753518249508414</id><published>2009-01-31T09:42:00.009+09:00</published><updated>2009-01-31T10:33:06.794+09:00</updated><title type='text'>Python旅館第一回</title><content type='html'>最近やる気が無かったのですが Python旅館に来たら嘘のようにやる気が復活しました。&lt;br /&gt;&lt;br /&gt;本当は原稿を書く予定だったんですが、隣に座った &lt;a href="http://twitter.com/tokibito"&gt;tokibito&lt;/a&gt; が &lt;a href="http://d.hatena.ne.jp/nullpobug/20090106/1231216470"&gt;IE6 の怖いコレ&lt;/a&gt; について教えてくれました。&lt;br /&gt;&lt;br /&gt;おお！こないだ公開した &lt;a href="http://www.orkut.com/Main#AppInfo.aspx?appUrl=http%3A%2F%2Ftriviads.appspot.com%2Fstatic%2Fmy-trivia.xml&amp;objs=&amp;sn=&amp;ref=SR"&gt;My Trivia&lt;/a&gt; にもこのセキュリティホールあるし。早速直さないと。この OpenSocial アプリ ではユーザ入力の Trivia をサーバ側から json で返して、OpenSocial Canvas 側で表示しているのですが、とりあえず表示の時に document.createTextNode() してるから大丈夫だろうと思っていました。&lt;br /&gt;&lt;br /&gt;しかしこういう時は &amp;lt; や &amp;gt; を下記のように処理してあげないといけないという事ですね。(この文脈では obj にユーザ入力文字が入ってます)&lt;br /&gt;&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;        obj = obj.replace('&lt;', r'\u003C')&lt;br /&gt;        obj = obj.replace('&gt;', r'\u003E')&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;しかしこんな簡単な修正に 1h くらいかかってしまって情けない...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6425753518249508414?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6425753518249508414/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6425753518249508414' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6425753518249508414'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6425753518249508414'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/01/httpd.html' title='Python旅館第一回'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-5902119084301319715</id><published>2009-01-06T10:49:00.003+09:00</published><updated>2009-01-06T10:55:35.053+09:00</updated><title type='text'>Blogger で Syntax Highlighting</title><content type='html'>明けましておめでとうございます。今年はすっかり寝正月でした。&lt;br /&gt;&lt;br /&gt;突然ですが Blogger で手軽に Syntax Highlighting を使えたので書いてみます。&lt;br /&gt;テンプレートのヘッダに下記を追加して&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;&amp;lt;link href='http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.css' rel='stylesheet' type='text/css'/&amp;gt;&lt;br /&gt;&amp;lt;script src='http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.js' type='text/javascript'/&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;body の onload ハンドラを設定します。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;&amp;lt;body onload='prettyPrint()'&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;実際のコード部には class 指定した pre タグを使います。&lt;br /&gt;&lt;pre class="prettyprint"&gt;&lt;br /&gt;&amp;lt;pre class="prettyprint"&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-5902119084301319715?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/5902119084301319715/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=5902119084301319715' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5902119084301319715'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5902119084301319715'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2009/01/blogger-syntax-highlighting.html' title='Blogger で Syntax Highlighting'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-1727002729090495694</id><published>2008-10-11T11:17:00.006+09:00</published><updated>2009-10-20T18:31:57.002+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='Maps API'/><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><title type='text'>GeoPt Property 用のカスタムウィジェット</title><content type='html'>こないだ Maps API hack-a-thon Tokyo に参加してきました。その時作ったのが Google App Engine の GeoPt Property をとても扱いやすくするための&lt;a href="http://paste.shehas.net/show/20/"&gt;ユーティリティクラス&lt;/a&gt;です。&lt;br /&gt;&lt;br /&gt;この記事では、そのクラスを使って geopoint データを datastore に保存するサンプルコードをお見せします。始めるまえに、こちらの&lt;a href="http://code.google.com/appengine/articles/djangoforms.html"&gt;djangoforms に関する記事&lt;/a&gt;を読んでおくとコードがすぐ理解できるでしょう。&lt;br /&gt;&lt;br /&gt;まずはこの記事用のディレクトリを作ります。そして拙作の &lt;a href="http://paste.shehas.net/show/20/"&gt;my_geopt.py&lt;/a&gt; をその中にコピーし、さらに新しく3つのファイルを作ります。app.yaml, main.py と index.html です。&lt;br /&gt;&lt;br /&gt;ファイルを作成したら、開発用サーバーを機動して localhost にアクセスします。すると Google Map をクリックするだけで特定の GeoPt が指定できる事がわかります。&lt;br /&gt;&lt;br /&gt;また &lt;a href="http://tm-test.appspot.com/"&gt;ライブデモ&lt;/a&gt;もあります(少し複雑になってますが、基本は同じです)。&lt;br /&gt;&lt;br /&gt;Happy coding :-)&lt;br /&gt;&lt;br /&gt;app.yaml&lt;br /&gt;&lt;pre class="prettyprint"&gt;application: geotest&lt;br /&gt;version: 1&lt;br /&gt;runtime: python&lt;br /&gt;api_version: 1&lt;br /&gt;&lt;br /&gt;handlers:&lt;br /&gt;- url: /.*&lt;br /&gt;  script: main.py&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;main.py&lt;br /&gt;&lt;pre class="prettyprint"&gt;from google.appengine.ext import webapp&lt;br /&gt;from google.appengine.ext.webapp.util import run_wsgi_app&lt;br /&gt;&lt;br /&gt;import os&lt;br /&gt;import wsgiref.handlers&lt;br /&gt;&lt;br /&gt;from google.appengine.ext import db&lt;br /&gt;from google.appengine.ext.webapp import template&lt;br /&gt;from google.appengine.ext.db import djangoforms&lt;br /&gt;import my_geopt&lt;br /&gt;&lt;br /&gt;class Place(db.Model):&lt;br /&gt;  name = db.StringProperty()&lt;br /&gt;  description = db.StringProperty()&lt;br /&gt;  geopoint = db.GeoPtProperty()&lt;br /&gt;  date = db.DateTimeProperty(auto_now=True)&lt;br /&gt;&lt;br /&gt;class PlaceForm(djangoforms.ModelForm):&lt;br /&gt;  class Meta:&lt;br /&gt;    model = Place&lt;br /&gt;&lt;br /&gt;class MainPage(webapp.RequestHandler):&lt;br /&gt;  def post(self):&lt;br /&gt;    data = PlaceForm(data=self.request.POST)&lt;br /&gt;    if data.is_valid():&lt;br /&gt;      # Save the data, and redirect to the view page&lt;br /&gt;      entity = data.save(commit=False)&lt;br /&gt;      entity.put()&lt;br /&gt;      self.redirect('/')&lt;br /&gt;    else:&lt;br /&gt;      # Reprint the form&lt;br /&gt;      contents = {'form': data}&lt;br /&gt;      path = os.path.join(os.path.dirname(__file__), 'index.html')&lt;br /&gt;      self.response.out.write(template.render(path, contents))&lt;br /&gt;  def get(self):&lt;br /&gt;    contents = {'form': PlaceForm()}&lt;br /&gt;    path = os.path.join(os.path.dirname(__file__), 'index.html')&lt;br /&gt;    self.response.out.write(template.render(path, contents))&lt;br /&gt;&lt;br /&gt;application = webapp.WSGIApplication(&lt;br /&gt;  [('/', MainPage),],&lt;br /&gt;  debug=True)&lt;br /&gt;&lt;br /&gt;def main():&lt;br /&gt;  run_wsgi_app(application)&lt;br /&gt;&lt;br /&gt;if __name__ == "__main__":&lt;br /&gt;  main()&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;index.html (Please replace 2 YOURAPIKEYs with your Maps API Key)&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"&amp;gt;&lt;br /&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;head&amp;gt;&lt;br /&gt;&amp;lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"&amp;gt;&lt;br /&gt;&amp;lt;title&amp;gt;GeoPtWidgets Example&amp;lt;/title&amp;gt;&lt;br /&gt;&amp;lt;script src="http://maps.google.com/maps?file=api&amp;amp;v=2&amp;amp;key=YOURAPIKEY"&lt;br /&gt;type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;script src="http://www.google.com/uds/api?file=uds.js&amp;v=1.0&amp;key=YOURAPIKEY"&lt;br /&gt;type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;script src="http://www.google.com/uds/solutions/localsearch/gmlocalsearch.js"&lt;br /&gt;type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;style type="text/css"&amp;gt;&lt;br /&gt;@import url("http://www.google.com/uds/css/gsearch.css");&lt;br /&gt;@import url("http://www.google.com/uds/solutions/localsearch/gmlocalsearch.css");&lt;br /&gt;&amp;lt;/style&amp;gt;&lt;br /&gt;&amp;lt;/head&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;body onunload="GUnload()"&amp;gt;&lt;br /&gt;&amp;lt;h1&amp;gt;GeoPtWidgets Example&amp;lt;/h1&amp;gt;&lt;br /&gt;&amp;lt;form method="post" action="/"&amp;gt;&lt;br /&gt;{{ form.as_p }}&lt;br /&gt;&amp;lt;input type="submit" value="register"/&amp;gt;&lt;br /&gt;&amp;lt;/form&amp;gt;&lt;br /&gt;&amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-1727002729090495694?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/1727002729090495694/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=1727002729090495694' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1727002729090495694'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1727002729090495694'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/10/geopt-property.html' title='GeoPt Property 用のカスタムウィジェット'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-5750922581048448067</id><published>2008-10-11T09:59:00.016+09:00</published><updated>2009-10-20T18:30:27.173+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='Maps API'/><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='google'/><title type='text'>A custom widgets for GeoPt Property</title><content type='html'>I attended a Maps API hack-a-thon in Tokyo. There I wrote some &lt;a href="http://paste.shehas.net/show/20/"&gt;utility class&lt;/a&gt; which helps us to handle GeoPt Property very easy.&lt;br /&gt;&lt;br /&gt;In this article, I'll show you some sample code which store geopoint data by just clicking on Google Map. Before we go, perhaps you can read the &lt;a href="http://code.google.com/appengine/articles/djangoforms.html"&gt;djangoforms documentation&lt;/a&gt;. This will help you to understand the codes in this article. &lt;br /&gt;&lt;br /&gt;Please create a directory for this article, copy &lt;a href="http://paste.shehas.net/show/20/"&gt;my_geopt.py&lt;/a&gt; to the directory and create 3 brand new files app.yaml, main.py, and index.html. That's all.&lt;br /&gt;&lt;br /&gt;After creating these files, start local dev server, get access to localhost, and see that you can specify a particular GeoPt by just clicking on the Google Map.&lt;br /&gt;&lt;br /&gt;And &lt;a href="http://tm-test.appspot.com/"&gt;here is a live demo&lt;/a&gt;(slightly more complicated, but basically almost the same).&lt;br /&gt;&lt;br /&gt;Happy coding :-)&lt;br /&gt;&lt;br /&gt;app.yaml&lt;br /&gt;&lt;pre class="prettyprint"&gt;application: geotest&lt;br /&gt;version: 1&lt;br /&gt;runtime: python&lt;br /&gt;api_version: 1&lt;br /&gt;&lt;br /&gt;handlers:&lt;br /&gt;- url: /.*&lt;br /&gt;  script: main.py&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;main.py&lt;br /&gt;&lt;pre class="prettyprint"&gt;from google.appengine.ext import webapp&lt;br /&gt;from google.appengine.ext.webapp.util import run_wsgi_app&lt;br /&gt;&lt;br /&gt;import os&lt;br /&gt;import wsgiref.handlers&lt;br /&gt;&lt;br /&gt;from google.appengine.ext import db&lt;br /&gt;from google.appengine.ext.webapp import template&lt;br /&gt;from google.appengine.ext.db import djangoforms&lt;br /&gt;import my_geopt&lt;br /&gt;&lt;br /&gt;class Place(db.Model):&lt;br /&gt;  name = db.StringProperty()&lt;br /&gt;  description = db.StringProperty()&lt;br /&gt;  geopoint = db.GeoPtProperty()&lt;br /&gt;  date = db.DateTimeProperty(auto_now=True)&lt;br /&gt;&lt;br /&gt;class PlaceForm(djangoforms.ModelForm):&lt;br /&gt;  class Meta:&lt;br /&gt;    model = Place&lt;br /&gt;&lt;br /&gt;class MainPage(webapp.RequestHandler):&lt;br /&gt;  def post(self):&lt;br /&gt;    data = PlaceForm(data=self.request.POST)&lt;br /&gt;    if data.is_valid():&lt;br /&gt;      # Save the data, and redirect to the view page&lt;br /&gt;      entity = data.save(commit=False)&lt;br /&gt;      entity.put()&lt;br /&gt;      self.redirect('/')&lt;br /&gt;    else:&lt;br /&gt;      # Reprint the form&lt;br /&gt;      contents = {'form': data}&lt;br /&gt;      path = os.path.join(os.path.dirname(__file__), 'index.html')&lt;br /&gt;      self.response.out.write(template.render(path, contents))&lt;br /&gt;  def get(self):&lt;br /&gt;    contents = {'form': PlaceForm()}&lt;br /&gt;    path = os.path.join(os.path.dirname(__file__), 'index.html')&lt;br /&gt;    self.response.out.write(template.render(path, contents))&lt;br /&gt;&lt;br /&gt;application = webapp.WSGIApplication(&lt;br /&gt;  [('/', MainPage),],&lt;br /&gt;  debug=True)&lt;br /&gt;&lt;br /&gt;def main():&lt;br /&gt;  run_wsgi_app(application)&lt;br /&gt;&lt;br /&gt;if __name__ == "__main__":&lt;br /&gt;  main()&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;index.html (Please replace 2 YOURAPIKEYs with your Maps API Key)&lt;br /&gt;&lt;pre class="prettyprint"&gt;&amp;lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"&amp;gt;&lt;br /&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;head&amp;gt;&lt;br /&gt;&amp;lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"&amp;gt;&lt;br /&gt;&amp;lt;title&amp;gt;GeoPtWidgets Example&amp;lt;/title&amp;gt;&lt;br /&gt;&amp;lt;script src="http://maps.google.com/maps?file=api&amp;amp;v=2&amp;amp;key=YOURAPIKEY"&lt;br /&gt;type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;script src="http://www.google.com/uds/api?file=uds.js&amp;v=1.0&amp;key=YOURAPIKEY"&lt;br /&gt;type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;script src="http://www.google.com/uds/solutions/localsearch/gmlocalsearch.js"&lt;br /&gt;type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;style type="text/css"&amp;gt;&lt;br /&gt;@import url("http://www.google.com/uds/css/gsearch.css");&lt;br /&gt;@import url("http://www.google.com/uds/solutions/localsearch/gmlocalsearch.css");&lt;br /&gt;&amp;lt;/style&amp;gt;&lt;br /&gt;&amp;lt;/head&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;body onunload="GUnload()"&amp;gt;&lt;br /&gt;&amp;lt;h1&amp;gt;GeoPtWidgets Example&amp;lt;/h1&amp;gt;&lt;br /&gt;&amp;lt;form method="post" action="/"&amp;gt;&lt;br /&gt;{{ form.as_p }}&lt;br /&gt;&amp;lt;input type="submit" value="register"/&amp;gt;&lt;br /&gt;&amp;lt;/form&amp;gt;&lt;br /&gt;&amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-5750922581048448067?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/5750922581048448067/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=5750922581048448067' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5750922581048448067'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5750922581048448067'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/10/custom-widgets-for-geopt-property.html' title='A custom widgets for GeoPt Property'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-7816661614766761898</id><published>2008-07-13T23:41:00.022+09:00</published><updated>2008-07-14T08:11:58.921+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>Using the newest zipped pytz on GAE</title><content type='html'>I posted &lt;a href="http://takashi-matsuo.blogspot.com/2008/07/using-zipped-pytz-on-gae.html"&gt;an entry about how to use zipped pytz on GAE&lt;/a&gt;[1].&lt;br /&gt;&lt;br /&gt;1. &lt;a href="http://takashi-matsuo.blogspot.com/2008/07/using-zipped-pytz-on-gae.html"&gt;http://takashi-matsuo.blogspot.com/2008/07/using-zipped-pytz-on-gae.html&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;It works well with older versions of pytz. Stefano pointed out that my method doesn't work with the newest pytz distribution. This article describes how to use the newest zipped pytz on GAE.&lt;br /&gt;&lt;br /&gt;First, retrieve the &lt;a href="http://pypi.python.org/pypi/pytz/"&gt;newest pytz from pypi&lt;/a&gt;[2] and extract the archive.&lt;br /&gt;&lt;br /&gt;2. &lt;a href="http://pypi.python.org/pypi/pytz/"&gt;http://pypi.python.org/pypi/pytz/&lt;/a&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;$ tar xjf pytz-2008c.tar.bz2&lt;br /&gt;$ cd pytz-2008c/pytz&lt;br /&gt;$ zip -q zoneinfo.zip `find zoneinfo -type f ! -name '*.pyc' -print`&lt;br /&gt;$ rm -rf zoneinfo&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;After that, you have to edit pytz/__init__.py and modify open_resource function to use zipped zoneinfo database like following:&lt;br /&gt;(pkg_resources stuff were struck out. Thank you again Stefano!)&lt;br /&gt;&lt;pre&gt;&lt;code&gt;&lt;br /&gt;def open_resource(name):&lt;br /&gt;"""Open a resource from the zoneinfo subdir for reading.&lt;br /&gt;&lt;br /&gt;&lt;strike&gt;Uses the pkg_resources module if available.&lt;/strike&gt;&lt;br /&gt;"""&lt;br /&gt;    import zipfile&lt;br /&gt;    from cStringIO import StringIO&lt;br /&gt;&lt;br /&gt;&lt;strike&gt;if resource_stream is not None:&lt;br /&gt;    return resource_stream(__name__, 'zoneinfo/' + name)&lt;br /&gt;else:&lt;/strike&gt;&lt;br /&gt;    name_parts = name.lstrip('/').split('/')&lt;br /&gt;    for part in name_parts:&lt;br /&gt;        if part == os.path.pardir or os.path.sep in part:&lt;br /&gt;            raise ValueError('Bad path segment: %r' % part)&lt;br /&gt;    zoneinfo = zipfile.ZipFile(os.path.join(os.path.dirname(__file__),&lt;br /&gt;        'zoneinfo.zip'))&lt;br /&gt;    return StringIO(zoneinfo.read(os.path.join('zoneinfo', *name_parts)))&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;br /&gt;Then, the only thing to do is to copying your original pytz directory into your application directory.&lt;br /&gt;&lt;code&gt;&lt;br /&gt;$ cd ../&lt;br /&gt;$ cp -r pytz /your/application/directory&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;If you'd like to avoid using CPU to unzip zoneinfo data every time, perhaps you could use following function:&lt;br /&gt;&lt;pre&gt;&lt;code&gt;&lt;br /&gt;from google.appengine.api import memcache&lt;br /&gt;import logging&lt;br /&gt;import pytz&lt;br /&gt;from pytz import timezone&lt;br /&gt;&lt;br /&gt;def getTimezone(tzname):&lt;br /&gt;  try:&lt;br /&gt;    tz = memcache.get("tz:%s" % tzname)&lt;br /&gt;  except:&lt;br /&gt;    tz = None&lt;br /&gt;    logging.debug("timezone get failed: %s" % tzname)&lt;br /&gt;  if tz is None:&lt;br /&gt;    tz = timezone(tzname)&lt;br /&gt;    memcache.add("tz:%s" % tzname, tz, 86400)&lt;br /&gt;    logging.debug("timezone memcache added: %s" % tzname)&lt;br /&gt;  else:&lt;br /&gt;    logging.debug("timezone memcache hit: %s" % tzname)&lt;br /&gt;&lt;br /&gt;  return tz&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Happy coding :-)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-7816661614766761898?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/7816661614766761898/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=7816661614766761898' title='12 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7816661614766761898'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7816661614766761898'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/07/using-newest-zipped-pytz-on-gae.html' title='Using the newest zipped pytz on GAE'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>12</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-4972763754527016524</id><published>2008-07-12T10:22:00.008+09:00</published><updated>2008-07-12T15:11:04.318+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='GAE'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>Using zipped pytz on GAE</title><content type='html'>To handle timezone of all over the world crrectly, I think it is the most easy way to use pytz[1]. Besides that, there are many modules and programs which depends on pytz modes, so I believe that quite a few people would like to use pytz on GAE. However, pytz contains over 500 files in its zoneinfo directory, so it often bears on the 1000 files limit.&lt;br /&gt;&lt;br /&gt;1. http://pytz.sourceforge.net/&lt;br /&gt;&lt;br /&gt;This is an instruction about how to use zipped pytz on GAE environment. In this article, we use my_zipimport.py[2] provided by Guido van Rossum.&lt;br /&gt;&lt;br /&gt;2. http://code.google.com/p/googleappengine/issues/detail?id=161#c19&lt;br /&gt;&lt;br /&gt;First, make your own pytz.zip as following:&lt;br /&gt;&lt;code&gt;&lt;pre&gt;&lt;br /&gt;$ cd pytz-2006p&lt;br /&gt;$ zip -q pytz.zip `find pytz -type f ! -name '*.pyc' -print`&lt;br /&gt;&lt;/pre&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Secondly, make sure to put my_zipimport.py and pytz.zip into your GAE app directory.&lt;br /&gt;Lastly, put following code into your script.&lt;br /&gt;&lt;code&gt;&lt;pre&gt;&lt;br /&gt;import my_zipimport&lt;br /&gt;import sys&lt;br /&gt;&lt;br /&gt;my_zipimport.install()&lt;br /&gt;sys.path.insert(0, 'pytz.zip')&lt;br /&gt;&lt;/pre&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;That's all. Now you can use pytz module seamlessly.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-4972763754527016524?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/4972763754527016524/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=4972763754527016524' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/4972763754527016524'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/4972763754527016524'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/07/using-zipped-pytz-on-gae.html' title='Using zipped pytz on GAE'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-1423696739990663601</id><published>2008-07-04T07:38:00.003+09:00</published><updated>2008-07-04T07:45:50.553+09:00</updated><title type='text'>Hackathon kit</title><content type='html'>&lt;a href="http://groups.google.com/group/google-appengine/browse_thread/thread/2f7dbf545aff5d7a/59f3aebd06461e5c?hl=en&amp;amp;lnk=gst&amp;amp;q=Translation+of+the+great+presentation#59f3aebd06461e5c"&gt;Google I/O のプレゼンを和訳したこれ使ってセミナーしても良い?&lt;/a&gt;と &lt;a href="http://groups.google.com/group/google-appengine?hl=en"&gt;本家グループ&lt;/a&gt; にメールしたら、ある US の googler からメールが来まして、なんと hackathon kit を提供してくれるようです。 わーい。&lt;br /&gt;&lt;br /&gt;という亊で 7月の終わりか8月の始めに hackathon or campfire やりたいな...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-1423696739990663601?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/1423696739990663601/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=1423696739990663601' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1423696739990663601'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1423696739990663601'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/07/hackathon-kit.html' title='Hackathon kit'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-5294455194916666598</id><published>2008-07-04T07:32:00.002+09:00</published><updated>2008-07-04T07:37:35.819+09:00</updated><title type='text'>Meet-at</title><content type='html'>&lt;a href="http://groups.google.com/group/google-app-engine-japan"&gt;Google-App-Engine-Japan グループ&lt;/a&gt; で Meet-at というオープンソースプロジェクトを始める際の共同開発者を募集したら結構反応がありました。かなり強力なメンバーが揃ったので、個人的にはとても期待しています。&lt;br /&gt;&lt;br /&gt;早速 7/5 にキックオフミーティングをやる予定です。どの技術を使おうとか相談するのもなかなか楽しいですが早くコーディングが始まらないかな...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-5294455194916666598?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/5294455194916666598/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=5294455194916666598' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5294455194916666598'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5294455194916666598'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/07/meet-at.html' title='Meet-at'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-1536480350279956131</id><published>2008-06-27T00:08:00.003+09:00</published><updated>2008-06-27T00:13:36.735+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><category scheme='http://www.blogger.com/atom/ns#' term='community'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>グループオーナー (続き)</title><content type='html'>&lt;a href="http://www.python.jp/pipermail/python-ml-jp/"&gt;python-ml-jp&lt;/a&gt; にて &lt;a href="http://groups.google.com/group/google-app-engine-japan"&gt;Google-App-Engine-Japan グループ&lt;/a&gt;を宣伝したら 30 人ほどメンバーが増えました!&lt;br /&gt;みなさんようこそいらっしゃいました。&lt;br /&gt;&lt;br /&gt;メンバーも増えた亊だし少し語りかけてみようかなと思います。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-1536480350279956131?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/1536480350279956131/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=1536480350279956131' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1536480350279956131'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/1536480350279956131'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/06/blog-post_27.html' title='グループオーナー (続き)'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-3266881921507809788</id><published>2008-06-25T22:18:00.003+09:00</published><updated>2008-06-25T22:32:49.056+09:00</updated><title type='text'>グループオーナー</title><content type='html'>さて、&lt;a href="http://groups.google.com/group/google-app-engine-japan"&gt;google-app-engine-japan グループ&lt;/a&gt;のオーナーになったわけでありますが、イマイチ人の集まりが悪い感じであります。やはり python や django のコミュニティへ一度宣伝をした方が良いのかもしれませんね。あとは python を使って無い人へアピールとかも必要なのかもしれません。&lt;br /&gt;&lt;br /&gt;実は勤めている社内で Google App Engine のコードラボを企画中なのですが、こちらでも思ったより人の集まりが悪いです。日本では python は馴染みが少ないからかも知れません(まあうちの会社の開発者は Java がメインというのもあるか)。&lt;br /&gt;&lt;br /&gt;ゆくゆくは一般向けにコードラボやキャンプファイアのような企画をやりたいと考えていますが、まずはグループの存在を知ってもらい参加してもらう方が先決ですね。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-3266881921507809788?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/3266881921507809788/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=3266881921507809788' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3266881921507809788'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3266881921507809788'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/06/blog-post_25.html' title='グループオーナー'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6044581401199104019</id><published>2008-06-13T11:38:00.003+09:00</published><updated>2008-06-13T11:41:21.588+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='IO2008'/><title type='text'>Google I/O セッション</title><content type='html'>I/O セッションのビデオとプレゼンが出ました!&lt;br /&gt;&lt;br /&gt;&lt;a href="http://sites.google.com/site/io/"&gt;http://sites.google.com/site/io/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;後でゆっくり見るぞ〜&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6044581401199104019?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6044581401199104019/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6044581401199104019' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6044581401199104019'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6044581401199104019'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/06/google-io.html' title='Google I/O セッション'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-7959355726109154632</id><published>2008-06-05T22:49:00.004+09:00</published><updated>2008-06-05T22:58:14.045+09:00</updated><title type='text'>App Engine ドキュメントの和訳</title><content type='html'>&lt;p&gt;&lt;a href="http://code.google.com/appengine/articles/gdata.html"&gt;Retrieving Authenticated Google Data Feeds with Google App Engine&lt;/a&gt;というドキュメントを和訳しました。&lt;a href="http://mars.shehas.net/%7Etmatsuo/misc/gdata-ja.html"&gt;Google App Engine 上で認証が必要な Google Data Feeds を取得する方法&lt;/a&gt;がそれです。いちおう公開前にオリジナル作者の Jeff Scudder さんに公開の許可を取っているので問題は無いはずです。&lt;/p&gt;この文書では App Engine 上で gdata-python-client ライブラリを有効に使う方法が解説されています。App Engine 上で gdata client を自由自在に使えれば、作成できるアプリケーションの幅がひろがりますね。&lt;br /&gt;&lt;br /&gt;今後も時間があればいろいろと翻訳していこうと思います。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-7959355726109154632?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/7959355726109154632/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=7959355726109154632' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7959355726109154632'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7959355726109154632'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/06/blog-post.html' title='App Engine ドキュメントの和訳'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-5588017510227466669</id><published>2008-06-03T16:01:00.003+09:00</published><updated>2008-06-03T19:50:46.077+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><title type='text'>App Engine の SMS verification</title><content type='html'>I/O で一般公開された(はずの) Google App Engine ですが、日本のみなさんは SMS の verification がなかなかできずに困っているのではないでしょうか。&lt;br /&gt;&lt;br /&gt;Verify のページでは「We are currently experiencing issues with DoCoMo and KDDI phones.」と表示されており、どうやら Docomo と KDDI では SMS が受け取れないようですし、私の持っている softbank 携帯でも SMS が受け取れませんでした。ちなみに嫁の softbank 携帯だと受け取る亊が出来ました。&lt;br /&gt;&lt;br /&gt;嫁は昔から softbank (Jフォン時代から)ひとすじで、私は昔 Docomo だったのを MNP で softbank に乗り換えたクチですが、それが関係してるのかな?&lt;br /&gt;&lt;br /&gt;ちなみに softbank 携帯に SMS を送った時は、Country and Carrier には「Other」を選び、Mobile Numberに「+81 90xxxxxxxx」(xは番号です)というように国番号「+81」の次に、始めの「0」は無しで電話番号を入力すれば送る亊ができました。参考になればと思い書いておきます。&lt;br /&gt;&lt;br /&gt;しかし、そろそろ Developper Day が開催するので、それまでにはなるべく多くの人が App Engine を使えるようになると良いのですが...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-5588017510227466669?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/5588017510227466669/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=5588017510227466669' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5588017510227466669'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/5588017510227466669'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/06/app-engine-sms-verification.html' title='App Engine の SMS verification'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-3918246650299978715</id><published>2008-06-01T10:35:00.002+09:00</published><updated>2008-06-01T10:39:43.878+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='google'/><category scheme='http://www.blogger.com/atom/ns#' term='web'/><title type='text'>My first app on App Engine</title><content type='html'>I attended the conference named 'Google I/O'. It was a very exciting event for an engineer like me. I attended many sessions that were, in most cases, related to Google App Engine.&lt;br /&gt;&lt;br /&gt;Google App Engine is a web platform where we can run our web application programs on Google's infrastructure. In my opinion, that could be the biggest paradigm shift this decade.&lt;br /&gt;&lt;br /&gt;In those sessions, I learned many techniques about how to build applications on Google App Engine, so I finally wrote up my first application on App Engine called 'InterNovel'. The URL is below.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://internovel.appspot.com/"&gt;http://internovel.appspot.com/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;In this application, there are novels. Each novel has its own state. The state is one of the following three: 1. Accepting fragments. 2. Votes. 3. Complete.&lt;br /&gt;&lt;br /&gt;1. Accepting fragments&lt;br /&gt;When the novel is in this state, everyone can post a continuous fragment. If the number of posted fragments reaches a particular count (that the owner of the novel decided beforehand), the state will change to 2.&lt;br /&gt;&lt;br /&gt;2. Votes&lt;br /&gt;When the novel is in Votes state, everyone can vote on their favorite fragments among the candidates. If the total number of the votes reaches a particular count (this number is also editable by the owner), the state will change to 1 or 3.&lt;br /&gt;&lt;br /&gt;3. Complete&lt;br /&gt;If the number of iterations between states 1 and 2 reaches a particular number, the completion comes. The state will never change from this time.&lt;br /&gt;&lt;br /&gt;Well, that's all about this application. Isn't it interesting?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-3918246650299978715?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/3918246650299978715/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=3918246650299978715' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3918246650299978715'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/3918246650299978715'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/06/my-first-app-on-app-engine.html' title='My first app on App Engine'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6006490704455480468</id><published>2008-05-30T19:58:00.004+09:00</published><updated>2008-05-30T20:13:59.316+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='App Engine'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='IO2008'/><category scheme='http://www.blogger.com/atom/ns#' term='google'/><title type='text'>Google I/O が終わりました</title><content type='html'>終わってしまいました。 Guido のセッションがものすごく High Level だったのが意外でした。 High Level というのは 「 core では無い」という意味ですが。&lt;br /&gt;&lt;br /&gt;全体的な感想ですが、今回は主な興味の対象である App Engine のセッションを中心に見てきまして、 App Engine についていろいろあやふやだった点がはっきりしてきました。とても有意義なセッション達でした。&lt;br /&gt;&lt;br /&gt;ところで今回同行した a2c さんが面白い亊を言っていました。興味の無いテクノロジーのセッションを聞くのも意外と楽しい...と。そういうものかもしれません。発見する亊はそもそも楽しいですからね。&lt;br /&gt;&lt;br /&gt;そうそう。以前のポストで書いた App Engine 用のソフトウェアがだいたい完成しましたので、URL を書いときます。&lt;br /&gt;&lt;a href="http://internovel.appspot.com/"&gt;http://internovel.appspot.com/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;まだ説明とか全然足りないので、多分正体や使い方も皆目分からないと思います。それらは日本に帰ってからおいおい書くつもりです。&lt;br /&gt;&lt;br /&gt;今日はとりあえずこんなところで。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6006490704455480468?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6006490704455480468/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6006490704455480468' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6006490704455480468'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6006490704455480468'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/05/google-io_30.html' title='Google I/O が終わりました'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-2908013813781613078</id><published>2008-05-25T21:36:00.009+09:00</published><updated>2008-05-30T20:35:38.401+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='japanese'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><category scheme='http://www.blogger.com/atom/ns#' term='google'/><title type='text'>Google I/O前夜</title><content type='html'>Google I/O が近づいて来ましたので準備をはじめました。まずはプログラムのスケジュールをダウンロードして印刷しておきます。これを飛行機の中で眺めておけば、参加したいプログラムをある程度しぼれるでしょう。荷物は少なめです。いつも使っているトラベルバッグに収まりそう。&lt;br /&gt;&lt;br /&gt;Google I/O 自体は 5/28, 5/29 の2日間なのですが、私自身は 5/27 のうちに San Francisco に到着予定です。この日の夕食をどうするか悩んでいたのですが、何人かの Googler がホストしてくれる亊になったので気が楽になりました :-)&lt;br /&gt;&lt;br /&gt;私個人として、今一番注目しているテクノロジーは &lt;a href="http://code.google.com/appengine/"&gt;App Engine&lt;/a&gt; です。Google のインフラを使って Web アプリが動かせるのですから、冗長化の面でもスケーラビリティの面でも大きなメリットがあります。それに python だし。 python は私のお気に入りです。&lt;br /&gt;&lt;br /&gt;そうそう。まだ内容は内緒ですが、 App Engine で動く Web アプリを作っている最中なのです。もう少しでプロトタイプが完成しそうなので、飛行機の中で書き終えられたら良いなと思っています。とりあえず動くものが完成したら &lt;a href="http://groups.google.com/group/google-appengine?hl=en"&gt;App Engine Group&lt;/a&gt; にポストしてみるつもりです。(5/26追記) グループにポストするよりも &lt;a href="http://appgallery.appspot.com/"&gt;Applications Gallery&lt;/a&gt; に登録する方が良さそうですね。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-2908013813781613078?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/2908013813781613078/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=2908013813781613078' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/2908013813781613078'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/2908013813781613078'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2008/05/google-io.html' title='Google I/O前夜'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6013794630289653841</id><published>2007-07-21T09:30:00.000+09:00</published><updated>2007-07-21T09:39:12.131+09:00</updated><title type='text'>GHeimdall is out!</title><content type='html'>This week, I released &lt;a href="http://code.google.com/p/gheimdall/"&gt;GHeimdall&lt;/a&gt; - A small web application for Google Apps SSO service. That was very exciting. It seems that there is no response without &lt;a href="http://groups.google.com/group/google-apps-apis/browse_thread/thread/23ff641b4569d79f?hl=en"&gt;Alex&lt;/a&gt;&lt;a href="http://groups.google.com/group/google-apps-apis/browse_thread/thread/23ff641b4569d79f?hl=en"&gt;'s&lt;/a&gt;. I hope someone will use it.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6013794630289653841?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6013794630289653841/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6013794630289653841' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6013794630289653841'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6013794630289653841'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2007/07/gheimdall-is-out.html' title='GHeimdall is out!'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-216908966551580953</id><published>2007-07-21T08:51:00.000+09:00</published><updated>2007-07-21T09:39:27.890+09:00</updated><title type='text'>http proxy is complicated</title><content type='html'>There were a post that gdata-python-client had no proxy support on &lt;a class="ln" href="http://groups.google.com/group/gdata-python-client-library-contributors?hl=en"&gt;GData Python Client Library Contributors&lt;/a&gt;.&lt;br /&gt;This week I had implemented the proxy support of that library and &lt;a href="http://groups.google.com/group/gdata-python-client-library-contributors/browse_thread/thread/4d865b309b5a50e5?hl=en"&gt;posted a patch&lt;/a&gt;. Jeff had looked into it and got it into the svn repository.&lt;br /&gt;&lt;br /&gt;Not until I wrote this code I was not aware of the complexity of http(s) proxy mechanism. If the final destination is http url, things are very simple. To acomplish the duty, just connect the proxy server and issue the http method as usual but with absolutely URL(started with 'http://') instead of partial URL(started with '/'). But the destination becomes https url, things get complicated. We have to use CONNECT method to open a ssl socket to dest server directly. And then issue the http method on this ssl socket.&lt;br /&gt;&lt;br /&gt;Ah...     explanation in English is very difficult.&lt;br /&gt;&lt;br /&gt;Anyway, I had learned little about proxy mechanism :)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-216908966551580953?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/216908966551580953/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=216908966551580953' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/216908966551580953'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/216908966551580953'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2007/07/http-proxy-is-complicated.html' title='http proxy is complicated'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-7836059527349503496</id><published>2007-06-15T23:17:00.000+09:00</published><updated>2007-06-15T23:31:57.330+09:00</updated><title type='text'>My codes were accepted!</title><content type='html'>My codes (provisioning API client in python) were accepted with praise :) I'm very glad.&lt;br /&gt;&lt;br /&gt;But this is just a start. I will write more open source codes.&lt;br /&gt;&lt;br /&gt;I hope that members of our company would write more open source codes, and submit it to the project. Actually, there are very few members who submit their codes to the outside world.  It is so lamentable.&lt;br /&gt;&lt;br /&gt;Because our company declare that we are 'Open Source Company',  so I think that more members should write open source codes.&lt;br /&gt;&lt;br /&gt;Happy coding,&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-7836059527349503496?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/7836059527349503496/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=7836059527349503496' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7836059527349503496'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7836059527349503496'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2007/06/my-codes-were-accepted.html' title='My codes were accepted!'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-7394149833884407664</id><published>2007-06-09T02:00:00.000+09:00</published><updated>2007-06-09T02:12:03.693+09:00</updated><title type='text'>Medical check, Voice trainning, MacOS9, and pretty 3 cats</title><content type='html'>As I wrote yesterday, I and my wife had took complete medical checkup. I hate drinking barium, and rectum check.&lt;br /&gt;&lt;br /&gt;Then I went to train my voice, and after that I went to my uncle's place to fix his computer troubles. There I met 3 cats which he feeds.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-7394149833884407664?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/7394149833884407664/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=7394149833884407664' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7394149833884407664'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/7394149833884407664'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2007/06/medical-check-voice-trainning-macos9.html' title='Medical check, Voice trainning, MacOS9, and pretty 3 cats'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2144470051777636547.post-6297873234976896580</id><published>2007-06-07T22:21:00.000+09:00</published><updated>2007-06-07T22:51:27.077+09:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>First post</title><content type='html'>I had finished to write Google Apps Provisioning API V2 client codes in python. I wish they will take it in the main tree. I have some confidence about that.&lt;br /&gt;&lt;br /&gt;I am writing tgsso too. What is tgsso? Tgsso is TurboGears Google Single Sign On. It's a little web application which enables you to authenticate Google Apps users by PAM. Its outline is almost done, but there are many things to do.&lt;br /&gt;&lt;br /&gt;By the way, tomorrow I and my wife will have complete medical checkup. I hope we both are in good health. So, we are banned from eating or drinking anything after 9pm. Sigh...&lt;br /&gt;&lt;br /&gt;Let's sleep early.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2144470051777636547-6297873234976896580?l=takashi-matsuo.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://takashi-matsuo.blogspot.com/feeds/6297873234976896580/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2144470051777636547&amp;postID=6297873234976896580' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6297873234976896580'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2144470051777636547/posts/default/6297873234976896580'/><link rel='alternate' type='text/html' href='http://takashi-matsuo.blogspot.com/2007/06/first-post.html' title='First post'/><author><name>tmatsuo</name><uri>http://www.blogger.com/profile/09975209669204600556</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>
