タグ「GAE コード サンプル」が付けられているもの

先日、COCOMOによる工数計算 および、ファンクションポイント を使った見積ツールを作成したが、それに Excel で結果をダウンロードする機能を組み込みたいと思う。

gae_excel01

pyExcelerator ではなくて xlwt を使う

Google Data サービスを利用すると、Google Document 等も作成できるようだが、まずは、Excel でダウンロードできるように。

Excel に結果をダウンロードできれば、使い勝手も上がるかな~と。

まず、Pythonから、Excelを使うには、pyExcelerator だと思い、したしらべしていたら、どうも、直接レスポンスのストリームに Excel ワークブックを書き込みできないようだ。

自宅サーバーなら、一時ファイルを作成する手もあるだろうが、相手はGAEだし、一時ファイルを作らなきゃいけないのはいただけない。

もう少し調べると、どうもすでに pyExcelerator はメンテナンスされておらず、そこからフォークした、xlwt の方が現在開発継続中で機能も豊富なようだ。そちらを使ってみることとする。

Zip インポート

GAE には、ファイルのアップロードに上限があり、ファイル数が多いときなどは、Zipインポートを使用すると、ファイル数を稼げる。

初回のインポート時に、展開されるが以降はメモリにキャッシュされ、同じインスタンスに対しては、その後オーバーヘッドは発生しないとのこと。

xlwt をダウンロード、解凍し、built/lib 以下のフォルダを zip 圧縮する。

gae_excel02

アプリケーションのルートフォルダに配置(Exclepse + PyDev)

gae_excel03

実装

試しに、xlwt のサンプル、xlwt-0.7.2\xlwt-0.7.2\xlwt\examples\mini.py  の内容を出力してみる。

sys.path.insert で、zip import を行う。

import sys
#sys.path.insert(0, 'gdata.zip')
sys.path.insert(0, 'xlwt.zip')

import xlwt

import os
from google.appengine.ext import webapp

class DownloadResponse(webapp.RequestHandler):
    def get(self):
        self.post()
        
    def post(self):
        self.response.headers.add_header("Content-Disposition", 'attachment; filename="foo.xls"' )
        wb = xlwt.Workbook()
        ws = wb.add_sheet('xlwt was here')
        wb.save(self.response.out)
        return None

Workbook.save メソッドに、Response.out オブジェクトを渡しているが、引数として、ファイル様オブジェクトをとるようだ。

CompoundDoc.py を参照。以下のようなシグネチャで、file_name_or_filelike_obj に対して呼ばれているのは、open と write

def save(self, file_name_or_filelike_obj, stream):

Response クラス の out は、StringIO なので、いけるだろう。

実行してみる。

gae_excel04 

ダウンロードのダイアログが開いた。

gae_excel05

とりあえず、ローカル環境で Excel を開くことができた。

あとは、力業かな。

Google App Engine 上で動く CMS を動かしてみる。 Google とは暫定和解。

Eclipse + PyDev を使う。

1.前提

1.1 App Engine Site Creator の入手

http://code.google.com/p/app-engine-site-creator/ から、app-engine-site-creator_1.1.1.zip  をダウンロードして解凍しとく。

1.2 Google App Engine にアプリケーションを作成

参照

2.Google App Engine Project の作成

2.1 新規プロジェクト ~ PyDev Google App Engine Project を作成

appeng_site_cre01

2.2 Project 名を決める

appeng_site_cre02

2.2 Google App Engine SDK のパスを指定

appeng_site_cre03

2.3 アプリケーションIDを指定し、Empty テンプレートを指定し、プロジェクトを作成

appeng_site_cre04

2.4 PyDev プロジェクトのソースフォルダに、解凍した、App Site Creator のファイル群をコピー

appeng_site_cre05

2.5 app.yaml の application に、GAEのアプリケーション名を設定

appeng_site_cre10

2.6 ローカル環境で起動してみる

コンテキストメニューから、Debug As - PyDev : Google App Run

appeng_site_cre06

起動したっぽい

appeng_site_cre07 

2.7 GAE へデプロイ

コンテキストメニューから、PyDev : Google App Engine

appeng_site_cre08

appcfg.py update をコマンドラインからではなく、PyDev上からできるようになった。便利

appeng_site_cre09

2.8 動きました。

めでたし。

appeng_site_cre11

appeng_site_cre14

3.問題への対処

3.1 日本語が表示できるように

こちらのサイト(App Engine Site Creator 日本語化 | マルコ式ネット白書)を参考に

/views/admin.py

page.content = request.POST['editorHtml']

page.content = unicode(request.POST['editorHtml'],'utf-8')

3.2 ロゴの画像が表示されない

/templates/themes/nautica05/base.html

2カ所

<img src="images/logo.gif" alt=""/>

<img src="/static/images/logo.png" alt=""/>

3.3 dojo のスクリプトエラー

dojo 1.2.3 が利用可能なようだが、1.2.0 のバグ?か、IE8 だと、スクリプトエラーが出る。

以下の箇所を1.2.0 → 1.2 (おそらく最新なんだろう)で、解消。 /static ディレクトリにダウンロードして取り込んでもよいかも。

/templates/admin

  • edit_page.html
  • edit_user.html
  • index.html
src="http://ajax.googleapis.com/ajax/libs/dojo/1.2.0/dojo/dojo.xd.js"

src="http://ajax.googleapis.com/ajax/libs/dojo/1.2/dojo/dojo.xd.js"

 

いじょ。

GAE上で、Django の welcome ページまではなんとか動かすことができたので、次は単純なアプリケーションを作る。

1. アプリケーションの骨格を作成

Eclipse上のソースフォルダをコマンドプロンプトで開き、python manage.py startapp を実行する。

geatest というアプリケーション名にする。

C:\Program Files\eclipse3.4R2

そうすると、アプリケーションの骨格が生成され、以下のような構成になる。

gaetest フォルダ(パッケージ)が作成され、models.py や views.py ファイルが生成される。

gae_django15

2. モデルの作成

モデルクラスを作成する。

Django 標準のモデルクラスは機能しないので、GAE Django ヘルパーが提供するモデルクラスと、GAEのデータストアプロパティを使用する。

Django 標準のモデルクラスは機能しないが、Django モデルと同様に取り扱うことができ、モデルはDjangoに登録される。

from appengine_django.models import BaseModel
from google.appengine.ext import db 

# Create your models here.
class GaeTest(BaseModel):
    message = db.StringListProperty()
    create_date = db.DateTimeProperty('data created')

2.1 モデルの有効化

GAE で Django を使用する場合、RDBMS を使えないので、モデルに対応するテーブル等は作る必要がない。

setting.py のINSTALLED_APPS に記述するだけで、有効となる。

INSTALLED_APPS = (
     'appengine_django',
     'gaetest',  # この行を追加
)

3. ビュー、テンプレートの作成

3.1 URLマッピングの作成

初期表示で、保存されたモデルの内容を表示し、追加ボタンでデータを追加するアプリケーションを想定。

タプルの1つ目にURLのパターン、2つ目にリクエストURLがパターンに一致する場合に処理するハンドラ(ビュー)を記述する。

urlpatterns = patterns('',
    (r'^gaetest/$',     'gaetest.views.index'),  # 初期表示
    (r'^gaetest/add/$', 'gaetest.views.add'),    # データ追加
)

3.2 ビューの作成

上記 urls.py にて、リクエストURLのパターンによって、処理をハンドリングする

リクエストを処理して、レスポンスを返す。このとき、データを取得し、コンテキストに埋め込み(以下の例では、render_to_response に隠蔽されているが)、テンプレートでそれを取り出して描画させる。

from django.shortcuts import render_to_response
from gaetest.models import GaeTest
import datetime

def index(request):
    g_list = GaeTest.gql('order by message')
    return render_to_response('gaetest/index.html', {'g_list':g_list})
    
def add(request):
    msg = request.POST['message']
    g = GaeTest(message=msg, create_date=datetime.datetime.now())
    g.save()
    g_list = GaeTest.gql('order by message')
    return render_to_response('gaetest/index.html', {'g_list':g_list})

3.3 テンプレートの作成

上記ビューで、コンテキストに埋め込まれたオブジェクトを利用して、画面描画を行う。

<form method="post" action="/gaetest/add/">
    message:<input name="message" />
    <input value="add" type="submit" />
</form>
{% if g_list %}
    <ul>
    {% for g in g_list %}
        <li>{{ g.message }}
    {% endfor %}
    </li></ul>
{% else %}
    <p>No GaeTest are available.</p>
{% endif %}    

3.4 ここまでのファイル構成

ここまでの作業で、以下のようなファイル構成になる

gae_django17

4.実行

よっしゃうごいた。

gae_django18

5. その他

5.1 管理ツール

Django の管理ツールは、RDBMS と密接に関連しており、RDBMS を持たないGAEでは、提供されない。

別途、/_ah/admin で管理インターフェースが提供される。

gae_django16

5.2 対話シェル

対話シェルにて、モデルなどのAPIを利用可能。

python manage.py shell コマンドを使用する。

C:\Program Files\eclipse3.4R2\workspace\typea-services\src>python manage.py shell
            :
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from gaetest.models import GaeTest
>>> import datetime
>>> g = GaeTest(message='first gae model.',create_date=datetime.datetime.now())
>>> g.save()
datastore_types.Key.from_path(u'GaeTest', 1, _app_id_namespace=u'typea-services' )

いじょ。

Python をちゃんと(?)触り始めて、2ヶ月くらいかな?

Python おもしろい!最初は違和感を感じていたインデントによって制御が変わっちゃうところとかも、もはや非常に小気味よく感じている。ロジックの組み換えがなんともしやすいんだなこれ。 for (int i=0; i<x; i++) ってループが書けない!? そんな構文使う必要すらないぜ! リスト内包表記!! なんてかわいいやつなんだ!

って感じです。プログラム書くのがまた楽しくなってきた。なんというか、思考を直接的にコードに落としこめるので早くて効率も良い。仕事でやるなら時間もかけられるのだけれど、隙間時間を見つけてやるしかない趣味(?)のプログラミングで使うにはぴったり。

・・・ なんだけど、GAEでDjangoを動かそうとして、なんとかWelcomeページが動くまで結構大変だった(といっても3時間くらいか)ので、メモ。

1.前提

  • GAEは、WSGIというPythonのWebフレームワークのインターフェース規約にのっとっているので、Djangoだけではなく、準拠したフレームワークを利用することができる。
  • GAEはSQLデータベースをもたないので、Djangoの標準モデルクラスは使用できないなどの制限がある。ただし、別の実装を用意している。

2.環境構築手順

基本的に、http://code.google.com/intl/ja/appengine/articles/appengine_helper_for_django.html に従い実行していった。

2.1 Python for Windows Extensionsのインストール

上記手順に、Windows上でGoogleが提供するSDKをインストーラを使用している場合は、 http://sourceforge.net/projects/pywin32/ をインストールしてSDKを自動検出できるようにする必要がある。

gae_django02

・・・との記述でインストールしてみたが、Eclipse + PyDev で開発しようとしているので、不要だったかも。

また別マシンに構成するときに確認。

2.2 GAE-Django ヘルパーの導入

http://code.google.com/p/google-app-engine-django から、最新版とおぼしき、appengine_helper_for_django-r86.zip をダウンロード。Deprecated となっているのが気になる・・・。

解凍して、Eclipse + PyDev のプロジェクトのソースフォルダに展開する。上記手順サイトいは、mysite ディレクトリ云々の記述があるが、これはDjango チュートリアルの例に準じた記述のためで別に何でも良い。

展開後、サーバーを起動したところ、起動しているっぽい。いくつかWARNINGが出ているが、気にする必要はないと、上記の手順書にあるので気にしない。

gae_django03

2.3 最初のエラー  : ImportError: No module named _multiprocessing

http://localhost:8080 にアクセスしたところ・・・

gae_django04

なんじゃこりゃ。まったくどうしていいかわからないので、http://d.hatena.ne.jp/tmatsuu/20090818/1250606215 の対処法に従ってみる。

SDK google_appengine/google/appengine/tools/dev_appserver.py に以下の行を追加
    '_multibytecodec',      '_multiprocessing',     # この行を追加      '_random',

2.4 2つ目のエラー :  Django 1.0 or greater is required!

GAE-Djangoヘルパーさんは、Django 1.0 以降を要求しているにもかかわらず、GAEの都合により、SDKに同梱しているバージョンは0.96のようだ。

Django 0.96 に対応するヘルパーは appengine_helper_for_django-r52.zip

現在の最新版 GoogleAppEngine 1.2.5. でも変わらず。

gae_django05

SDKのDjangoは使わずに、最新の Django-1.1.tar.gz 別途ダウンロードしてプロジェクトの配下に配置してみる。

2.5 3つ目のエラー :  TypeError: Initialization arguments are not supported

さすがにちょっと疲れ始める。ググると、どうやら新たしいバージョンで問題が出ているようだ。1つ前のリリース Django-1.0.3.tar.gz を入手し展開したところ。

gae_django08

ここまでのバージョンのおさらい

  1. Google App Engine SDK ・・・ 1.2.5
  2. Django ・・・ 1.0.3
  3. Django ヘルパー ・・・ 0.86

2.6 It worked #1

さて実行!

gae_django09

おっしゃ。とりあえずWelcomeページが起動した。

2.7 このままじゃ・・・

プロジェクト配下にDjangoを置いたが、GAEでは、ファイル数に上限(1000)があり、このままでは発行できないようだ。zipimport という機能をつかうことによって、zipファイルからPythonモジュールを読み込むことができるため、djangoフォルダをzipすることで1つにでき、上記制限をクリアすることができる。お得だ。ただ、GAE版 zipimport は pycが使えないなどの制約があるよう。ま、とりあえずいいか。

djangoフォルダをルート直下に含んだ形になるように圧縮する。

gae_django11

main.py に以下の記述を追加。

# Add Django 1.0 archive to the path. django_path = 'django.zip'

sys.path.insert(0, django_path)

2.8 It worked #2

再度実行してみる。よしっ

gae_django12

3. 発行

C:\Program Files\Google\google_appengine>appcfg.py update "C:\Program Files\eclipse3.4R2\workspace\typea-services\src"

としたところ、またエラー・・・

You do not have permission to modify this app (app_id=u'google-app-engine-django').

おっと、アプリケーションIDを修正する必要があった。

gae_django13

4. It worked !!

gae_django14

結構つかれたぞ。

さぁ、だいぶピースがそろってきた。

当初は、こんな状況をもくろんでいたが、まぁ大体近い感じになるんじゃないかなー。

mixi_app_img

ただ、まぁー面白いんだけど、登場人物大杉。

mixiからTwitter検索して、Amazonの広告を表示するのに、JavaScript、HTML、CSSなどは置いておいても、最低これだけ登場してくる。

まぁードキュメント参照するのが大変でブラウザタブだらけ。ブックマークの管理も大変。

mixi_app_env01

ただ、やっとそれぞれが協力しあえるようになってきた。

GEAで作成したAmazon検索mixiアプリのTwitter検索に付け足してみる。

実際に、付け足すコードは、作成したGAE のサービスを呼び出して、結果HTMLを描画するだけ。(以下の青字部分)

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
  <ModulePrefs title="twitter search sample">
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>
  <Content type="html">
     <![CDATA[
        <style type="text/css">
            a:active, a:hover, .b:active, .link:active {
                color:#aaaaaa;
                text-decoration:underline;
            }
            a, .link {
                color:#2694E8;
                cursor:pointer;
                text-decoration:none;
            }      
        </style>
        <script type="text/javascript">
            /**
             * Amazon Web Services 検索処理
             */
            function search_amazon() {
                var url = "
http://typea-mixi01.appspot.com/am_is"
                            +  makeHttpParam('q', true);
                document.getElementById('amazon').innerHTML = url;
                var params = {};
                params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.GET;
                params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.TEXT;
                gadgets.io.makeRequest(url, amazonResponse, params);
                document.getElementById('amazon').innerHTML = "now seaching・・・"
            }
            /**
             * Amazon Web Services 検索結果処理
             */
            function amazonResponse(responseObj) {
                var html = "";
                if (responseObj.data != null) {
                    html = responseObj.data;
                }
                document.getElementById('amazon').innerHTML = html;
            }

            /**
             * Twitter 検索処理
             * @see http://apiwiki.twitter.com/Twitter-Search-API-Method%3A-search
             * @see http://code.google.com/intl/ja/apis/gadgets/docs/remote-content.html#Fetch_JSON
             */
            function search_twitter(url) {
                var baseurl = "http://search.twitter.com/search.json";
                if (url) {
                    url = baseurl + url;
                } else {
                    url = baseurl + makeHttpParam('q', true);
                }
                var params = {};
                params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.GET;
                params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
                gadgets.io.makeRequest(url, searchResponse, params);
            }

            /**
             * Twitter 検索結果処理
             */
            function searchResponse(responseObj) {
                var html = "";
                var jsondata = responseObj.data
                var next_page = jsondata['next_page'];
                var html_next = "";
                if (next_page) {
                    html_next = "<a href='javascript:search_twitter(\""

                              + next_page + "\");'>&gt;&gt;&nbsp;next page</a>"
                              + "<br/>";
                    html += html_next
                }
                var results = jsondata['results'];
                html +="<table border='0'>";
                for (var i=0; i<results.length; i++) {
                    var result = results[i];
                    html += "<tr style='font-size:small;"

                         + ((i % 2 == 0)?"":"background-color:#dfffff")+ "'>";
                    html +=   "<td>";
                    html +=     "<a href='http://twitter.com/"

                                    + result['from_user'] + "' target='_blank'>";
                    html +=         "<img src='" + result['profile_image_url'] + "' border='none'/>";
                    html +=     "</a>";
                    html +=   "</td>";
                    html +=   "<td>";
                    html +=     "<a href='http://twitter.com/" + result['from_user']

                                  + "' target='_blank'>";
                    html +=       "<span style='color:#2FC2EF;font-weight:bold;'>"

                                   + result['from_user'] + ":</span>"
                    html +=     "</a>";
                    html +=     createLink(result['text']) + "</br>";
                    html +=   "</td>";
                    html += "</tr>";
                }
                html += "</table>";
                html += html_next;
                document.getElementById('content_div').innerHTML = html;
            }
            function createLink(text) {
                return toUserUrlText(toFuzzyUrlText(text + ' '));
            }
            /** ちょっといい加減にURLをリンクに変更する関数 */
            function toFuzzyUrlText(text) {
                var ret = text;
                var ptn = /(http:\/\/.*?)[ $]/g; // 行末にマッチしない???
                var ary = ptn.exec(text);
                while(ary) {
                    ret = ret.replace(ary[0],

                        "<a href='" + RegExp.$1 + "' target='_blank'>" + ary[0] + "</a>");
                    ary = ptn.exec(text);
                }
                return ret;
            }
            /** ちょっといい加減にTwitter ID をリンクに変更する関数 */
            function toUserUrlText(text) {
                var ret = text;
                var ptn = /@([A-Za-z]{1,}?):/g;
                var ary = ptn.exec(text);
                while(ary) {
                    ret = ret.replace(ary[0],

                          "<a href='http://twitter.com/" + RegExp.$1

                          + "' target='_blank'>" + ary[0] + "</a>");
                    ary = ptn.exec(text);
                }
                return ret;
            }
            /**
             * HTTP GETリクエストパラメータを生成
             */
            function makeHttpParam(param_id, isFirstParam) {
                isFirstParam = !(isFirstParam == undefined);
                var paramObj = document.getElementById(param_id);
                var ret = "";
                if (paramObj != null) {
                    ret = ((isFirstParam)?"?":"&") + param_id + "="
                         + encodeURIComponent(paramObj.value);
                }
                return ret;
            }
        </script>
        <input type="text" size="16" id="q"/>

        <input type="button" name="search" value="twitter search"

            onclick="javascript:search_amazon();javascript:search_twitter();"/>
        <div id="amazon"></div>
        <div id="content_div"/>
     ]]>
  </Content>
</Module>

できました

mixi_twi_gae01

先日、Amazon Product (以下略) に、署名を組み込むことができたので、これも先日動作確認がとれた、Google App Engine for Pythonに組み込んでみる。

Python をやりだして間がないので、非常識な実装をしているかも・・・ が、まぁ気にせずに。

先日署名を組み込み、Amazon Web Services へリクエストするREST URLの生成、ブラウザでの動作確認ができたので、後やるべきことは、

  1. REST URL をリクエストして、レスポンスを取得する
  2. 取得したレスポンス(XML)を解析する
  3. 解析した内容からHTMLを作成する
  4. Google App Engine に組み込む
  5. 完成!

てな感じ。

以下詳細を記述

1.REST URL をリクエストして、レスポンスを取得

urllib2を利用する

import urllib2
    :
f = urllib2.urlopen(url)
f.read()

これだけ。んー楽だ。Pythonの人がJavaをまどろっこしがるのもわかる。

2.取得したレスポンス(XML)を解析する

XPathなども標準で使えるようだけど、まずは、単純な例なので、SAXパーサーを利用する。 この例を見ればやるべきことは一目瞭然。

ただ、解析しながら結果を生成していくのに、ハンドラ関数の外側で状態を管理しておきたいのだけれど、Pythonの変数とスコープの取り扱い方の常識が今ひとつわかっていない。グローバル(ファイル?)レベルでとりあつかっていいものなのかしら?サブクラス化したパーサーのインスタンスを作れるといいのだけれど

インスタンスのメソッドとしてハンドラを持たせて、状態をインスタンスで管理することが可能。メソッドの第1引数には self を設定する必要があるので、シグネチャが変わるので無理だと思い込んでた。

>>> class Foo:
...     f1 = ''
...     def bar(self, msg):
...             print msg
...             self.f1 = 'called'
...     def status(self):
...             print self.f1
...
>>> f =  Foo()
>>> x = f.bar
>>> x('test')
test
>>> f.status()
called


import urllib2
import xml.parsers.expat
# 要素の開始を処理するごとに呼び出されるハンドラ関数の定義
def StartElementHandler(name, attributes):
         :
# 要素の終端を処理するごとに呼び出されるハンドラ関数の定義
def EndElementHandler(name):
          :
# 文字データを処理するときに呼びだされるハンドラ関数の定義
def CharacterDataHandler(data):
          :
# XMLParserの生成
p = xml.parsers.expat.ParserCreate()
# 生成したParserにハンドラをセット
p.StartElementHandler = StartElementHandler
p.EndElementHandler = EndElementHandler
p.CharacterDataHandler = CharacterDataHandler
# リクエストの実行
f = urllib2.urlopen(url)
# 解析
p.Parse(f.read())

上記、2で、XMLを解析しながら結果オブジェクトを作っておいて、それからHTMLを生成

4.Google App Engine に組み込む

上記手順を踏まえて、このあたりの雛形に、Amazon Web Servicesの呼び出しを組み込む。

import している、amazon_ecs は、前回作成したもの。 ItemSearch だけ実行できる状態。

localhostで、実行確認できたら、デプロイする

5.完成!

#!Python2.6
# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

import amazon_ecs
import urllib2
import xml.parsers.expat

class SearchedItem:
    ''' Amazon ItemSearch Operation の結果格納  '''
    def __init(self)__:   

        self.asin = ''
        self.detailPageURL = ''
        self.smallImageURL = ''

class SAXTagHandler:
    def __init__(self):
        # XMLParser(SAX)の状態制御
        self.proc_start = False
        self.img_start = False
        self.now_key = ''
        self.item_list = []
    def startElementHandler(self, name, attributes):
        ''' xml.parsers.expat XMLParser のハンドラ
                    要素の開始を処理するごとに呼び出される
            @see http://www.python.jp/doc/release/lib/xmlparser-objects.html
        '''
        if name == 'Items':
            self.proc_start = True
        if name == 'SmallImage':
            self.img_start = True
        if self.proc_start:
            if name == 'Item':
                self.item_list.append(SearchedItem())
            else:
                self.now_key = name
    def endElementHandler(self, name):
        ''' xml.parsers.expat XMLParser のハンドラ
                    要素の終端を処理するごとに呼び出される
            @see http://www.python.jp/doc/release/lib/xmlparser-objects.html
        '''
        if name == 'Items':
            self.proc_start = False
        if name == 'SmallImage':
            self.img_start = False
    def characterDataHandler(self, data):
        ''' xml.parsers.expat XMLParser のハンドラ
                    文字データを処理するときに呼びだされる
            @see http://www.python.jp/doc/release/lib/xmlparser-objects.html
        '''
        if self.proc_start:
            idx = len(self.item_list)
            idx = idx -1
            if idx >= 0:
                if self.now_key == 'ASIN':
                    self.item_list[idx].asin = data
                if self.now_key == 'DetailPageURL':
                    self.item_list[idx].detailPageURL = data
                if self.img_start and self.now_key == 'URL':
                    self.item_list[idx].smallImageURL = data

class MainPage(webapp.RequestHandler):
    def get(self):
        self.redirect('/am_is?q=amazon')

class AmazonItemSearch(webapp.RequestHandler):
    ''' Amazon Product Advertising API を利用し、キーワード検索を行う
        example http://typea-mixi01.appspot.com/am_is?q=book
    '''
    def get(self):
        #パラメータの切り出し request.getが機能しない
        #keyword = self.request.get('q')
        keyword = ''
        queries = self.request.query_string.split('&')
        for query in queries:
            pair = query.split('=')
            if pair[0] == 'q':
                keyword = pair[1]
                break;

        #パラメータのデコード
        #@see http://www.findxfine.com/default/495.html
        #FireFox のアドレスバーに漢字を打つとUTF-8でないコードにエンコードされてしまう?
        keyword = urllib2.unquote(keyword) #.encode('utf-8')
        if keyword == '':
            keyword = 'amazon'
        operation = amazon_ecs.ItemSearch()
        operation.keywords(keyword)
        operation.search_index('Books')
        operation.response_group('Small')
        request = operation.request()

        # XMLParserの生成とハンドラのセット
        h = SAXTagHandler()
        p = xml.parsers.expat.ParserCreate()
        p.StartElementHandler = h.startElementHandler
        p.EndElementHandler = h.endElementHandler
        p.CharacterDataHandler = h.characterDataHandler

        # リクエストの実行と解析
        f = urllib2.urlopen(request)
        p.Parse(f.read())
        for itm in h.item_list:
            self.response.out.write('<a href="%s" style="padding-left:2px" target="_blank"><img src="%s" border="0"/></a>'
                                    % (itm.detailPageURL, itm.smallImageURL))
application = webapp.WSGIApplication([
                                      ('/', MainPage),
                                      ('/am_is', AmazonItemSearch)
                                      ], debug=True)

def main():
    run_wsgi_app(application)

if __name__ == "__main__":
    main()

完成しました ただ、本のサムネイルを横に並べるだけですが。

gae_py01

また一歩野望に近づいた!

昨年の9月以来、約10ヶ月ぶりにGAE Pythonに戻ってきた。

Antで、GAE Python用のウェブサーバーを起動させたりしていたが、心機一転環境を作り直したら、PyDev が、その辺のところに対応しているではないですか。

以下、手順。

Pythonのインストール、Eclipse 3.4 のインストール、Pydevのインストールは済ませて、以下の手順で、GAE用のプロジェクトを作成できる。

1. File - New - Other から、Pydev Google App Engine Projectを選択

gae_pydev01

2. プロジェクト名等を設定して Next

gae_pydev02

3. ここで、Google App Engine SDK for Python  のインストールパスを指定する

gae_pydev03

4. アプリケーションのID(おそらくGAEに登録したAppのIDだろう)と、テンプレートを選択(Hello Webapp World にしとくとGAE用のコードが生成される)

gae_pydev04

5. そのまま動かしてみる。ファイルのコンテキストメニューから、Debug As - Debug Configurations を選択

gae_pydev05

6. Google App Engine の 開発用Webサーバーの指定をするのだが、ここでちょっとはまった。

Main Moduleに開発用Webサーバーを指定するのだが、ブラウズボタンでは一向に指定できない!

直接書き込む必要があるみたい

C:\Program Files\Google\google_appengine\dev_appserver.py

gae_pydev06

7. Arguments タブを選択し、Program arguments に、プロジェクトの場所を指定

"${project_loc}/src"

gae_pydev07

8. Apply、Debug 押下でサーバーが起動するので、http://localhost:8080 にアクセス

gae_pydev08

無事起動された!

9. プロジェクトの登録

では、その勢いで、デプロイしてみる。コマンドプロンプトから、appcfg.py update を実行

C:\Program Files\Google\google_appengine>appcfg.py update "C:\Program Files\eclipse3.4R2\workspace\typea-test\src" 
C:\Program Files\Google\google_appengine\appcfg.py:40: DeprecationWarning: the sha module is deprecated; use the hashlib module instead  DIR_PATH,Scanning files on local disk. 
Initiating update. 
Email: pppiroto@gmail.com 
Password for pppiroto@gmail.com: 
Cloning 1 application file. 
Deploying new version. 
Checking if new version is ready to serve. 
Will check again in 1 seconds. 
Checking if new version is ready to serve. 
Will check again in 2 seconds. 
Checking if new version is ready to serve. 
Closing update: new version is ready to start serving. 
Uploading index definitions. 

↑ も毎回コマンドラインをたたくのは、わずらわしいのでANTタスクにしとく

<project basedir=".."> 
    <property name="project_location" value="typea-test\src"/> 
    <property name="appengine_dir" value="C:\Program Files\Google\google_appengine" /> 
    <property name="appcfg" value="appcfg.py" /> 
    <property name="project_update_cmd" value="${appengine_dir}\${appcfg}" /> 
    <target name="Release to GAE"> 
        <echo>Execute Command : ${project_update_cmd}</echo> 
        <echo>Target Directory : ${basedir}\${project_location}</echo> 
        <exec executable="python"> 
            <arg value='"${project_update_cmd}"'/> 
            <arg value='update'/> 
            <arg value='"${basedir}\${project_location}"'/> 
        </exec> 
    </target> 
</project>

  gae_pydev11

実行したところ。成功!

2回目以降はEmailとパスワード聞かれないのか?まぁいいや。

10. アップロードされた!

 gae_pydev09

11. サービスイン!

 http://typea-test.appspot.com/

成功!

gae_pydev10

注意!!

「App Engine にアップロードしたアプリケーションを削除する手段は提供されていません。1 つの Google アカウントで、最大 10 個のアプリケーション ID を登録できます。このうち 1 つをチュートリアルで消費したくない場合、このセクションは読むだけにし、最初のアプリケーションをアップロードする準備ができたときに参照してください。」

らしい・・・ はやくいってよ。無駄遣いしちゃいましたよ。

datastore01 

  • App Engine は、Python 用の データ モデリング API を備えている
  • Django のデータ モデリング API と似ているが、App Engine の拡張可能なデータストを背後で使用
  • import 文を最初の行に追加(1)
  • プロパティの種別はデータストアAPIリファレンス参照(2)
  • 新しいオブジェクトをデータストアに保存(3)
  • 保存されたオブジェクトをGQLで取り出す(4)
  • クラスのgqlメソッドでもデータを取得できる。(SELECT * FROM Person を省略できる
    • persons = Person.gql("ORDER BY id LIMIT 10")
  • WHERE句に定数は指定できない。パラメータを位置、名前で指定できる。
    • persons = Person.gql("WHERE id = :1 ORDER BY id",  int("1"))
    • persons = Person.gql("WHERE id = :id ORDER BY id",  id=int("2"))
import cgi 
import wsgiref.handlers 
from google.appengine.api import users 
from google.appengine.ext import webapp 
from google.appengine.ext import db  #(1) 
# 保存するモデルクラス 
class Person(db.Model): 
    id = db.IntegerProperty() #(2) 
    name = db.StringProperty() 
    mail = db.EmailProperty() 
class MainPage(webapp.RequestHandler): 
    def get(self): 
        self.response.out.write('<html><body>') 
        # (4) 保存されたデータを GQL で取り出す 
        persons = db.GqlQuery("SELECT * FROM Person ORDER BY id LIMIT 10") 
        self.response.out.write("<form action='/add' method='post'>")            
        self.response.out.write("""<table border='1'> 
            <tr> 
                <th>id</th><th>name</th><th>mail</th> 
            </tr> 
        """) 
        for person in persons: 
            self.response.out.write("<tr>") 
            self.response.out.write("<td>%d</td>" %person.id) 
            self.response.out.write("<td>%s</td>" %cgi.escape(person.name)) 
            self.response.out.write("<td>%s</td>" %cgi.escape(person.mail)) 
            self.response.out.write("</tr>") 
        self.response.out.write(""" 
            <tr> 
                <td><input type='text' size='2'  name='id'></td> 
                <td><input type='text' size='10' name='name'></td> 
                <td><input type='text' size='15' name='mail'> 
                    <input type='submit' value='add'></td> 
            </tr> 
            </table> 
        """) 
        self.response.out.write("</form>") 
        self.response.out.write('</body></html>') 
# データ投入処理        
class AddPersonPage(webapp.RequestHandler): 
    def post(self): 
        person = Person() 
        id = self.request.get('id') 
        if id != '': 
            person.id  = int(id) 
            person.name = self.request.get('name') 
            person.mail = self.request.get('mail') 
            person.put() # (3)データ保存 
        self.redirect('/') 
def main(): 
    application = webapp.WSGIApplication( 
                                         [('/', MainPage), 
                                          ('/add', AddPersonPage)], 
                                          debug=True 
                                         ) 
    wsgiref.handlers.CGIHandler().run(application) 
if __name__ == "__main__": 
    main() 

Http Form パラメータの処理。

以下参照。

  • POSTメッセージハンドラ
  • HTML特殊文字のエスケープ
  • FORMパラメータ取得
import cgi 
import wsgiref.handlers 

from google.appengine.api import users
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):
def get(self):
self.response.out.write("""
<html>
<body>
<form action="/msg" method="post">
name:<input type="text" name="name" value=""><br>
msg :<textarea name="message"></textarea><br>
<input type="submit">
</form>
</body>
</html>
""")

class MessagePage(webapp.RequestHandler):
def post(self): # POSTメッセージハンドラ
self.response.out.write('<html><body>')
self.response.out.write('name:'
+ cgi.escape( # HTML特殊文字のエスケープ
self.request.get('name')) # FORMパラメータ取得
+ '<br>')
self.response.out.write('message:<pre>'
+ cgi.escape(
self.request.get('message'))
+ "</pre>")
self.response.out.write('</body></html>')
def main():
application = webapp.WSGIApplication(
[('/', MainPage),
('/msg', MessagePage)],
debug=True
)
wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
main()

http://code.google.com/intl/ja/appengine/docs/gettingstarted/usingusers.html

Googleのユーザアカウントを利用できるみたい。

userservice01 

import wsgiref.handlers

from google.appengine.api import users
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):
def get(self):
# ユーザーがアプリケーションにログインしている場合、
# ユーザーの User オブジェクトを返す
user = users.get_current_user()
if user:
self.response.headers['Content-Type'] = 'text/plain'
self.response.out.write('Hello,' + user.nickname())
else:
# ユーザーがログインしていない場合、ユーザーのブラウザを
# Google アカウントのログイン ページにリダイレクトするように指示
# リダイレクトには、このページへの URL (self.request.uri) が
# 含まれているため、ユーザーは Google アカウント ログイン
# システムにより、ログインまたは新規アカウントの作成後、

#このページへ戻される
self.redirect(users.create_login_url(self.request.uri))

def main():
application = webapp.WSGIApplication(
[('/', MainPage)],
debug=True)
wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
main()

実行すると、ダミーのログインページが表示された。

 userservice02

ログインすると、ユーザー個別のメッセージが表示された。

userservice03

今日はここまで。

Google App Engine ことはじめ 野望(1)

EclipseにPythonプラグインをこの間導入し、書き方もちょっとづつ勉強しながら、本題の Google App Engine を触りたいと思う。

まずは、SDKのダウンロードから、フレームワークを使った簡単なアプリケーション作成までのメモ。Eclipseを使うこと以外は、基本的にこの手順にしたがう。

SDKのダウンロード

http://code.google.com/intl/ja/appengine/downloads.html

から、SDKをダウンロードして、インストーラの指示通りインストールを行う。

appeng01

Hello world!のコーディング

hello.py

print 'Content-Type: text/plain' 
print '' 
print 'Hello Google App Engine!' 

app.yaml

application: hellogoogleapp
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
script: hello.py

 

コマンドラインからだと、

>%sdk_install_dir%\google_appengine\dev_appserver.py hellogoogleapp/

のように起動するみたいだけど、Eclipseを使いたいので、 以下のbuild.xmlのようにAntのタスクを定義して起動させてみることにした・・・で、起動したはいいけど、どうやって停止させよう?。コマンドラインから起動の場合、Ctrl+Breakで停止するのだが、EclipseのコンソールからCtrl+Breakでも停止しないし。。。当面タスクマネジャーからプロセスを殺そう(弊害あるかな?)

build.xml

<project basedir="../">
  <property value="C:\Program Files\Google\google_appengine"

name="websvrdir"></property>

  <target name="run_webserver">    

<exec dir="${websvrdir}" executable="python">      

<arg line="dev_appserver.py '${basedir}/hellogoogleapp/' />    

</exec>

  </target> </project>

appeng02

で、Antタスクを実行し、SDK組み込みWebサーバを起動、そして、http://localhost:8080 にアクセス。めでたく起動!

appeng03

Webアプリケーション用フレームワークの利用

とりあえず、Python用のWebアプリケーションフレームワークも数々あり、DjangoCherryPyPylonsweb.pyをサポートしているそうだ・・・Google App Engine 以外での利用も考えたら、メジャーどころを押さえときたいところだけど、なにぶん知識がとぼしいので、ここは入門ページの教えに従って、「App Engine には、シンプルな独自の Web アプリケーション フレームワークが用意されています。これが webapp です。」を利用してみる。

 

コード補完させる

IDEなしではコーディングできない軟弱な体になっているので、Eclipseにコード補完させる。

Menu - Project - Properties - External Source Folders
に、SDKのインストールパス(例 C:\Program Files\Google\google_appengine)を指定

appeng04

ゆおし!補完できるようになった。

appeng05

サンプルコードの実装

appeng06

実装して、実行するのはいいが何度やってもエラーになる。。。

よく見てみたら、インデントのレベルがずれてた。

なれるとインデントで構造が決まる文法ってよくなるのかな~

中括弧のほうがいいな~

と思いつつ、コードを修正して実行。

appeng07

ちゃんと動きました。

また一歩野望に近づいた!!

Eclipse Python プラグイン PyDevの導入

http://www.eclipse.org/downloads/

eclipse-jee-ganymede-win32.zip

Eclipseの新しいやつ(3.4)のWeb開発版パッケージをダウンロード(Web開発版でなくてもよい)してインストール(解凍して適当なフォルダに置く)

eclipseee01

http://www.python.org/download/

python-2.5.2.msi

Pythonのランタイムをダウンロードして、動作確認

py01

py02

http://pydev.sourceforge.net/download.html

org.python.pydev.feature-1_3_20.zip

プラグインのダウンロードとインストール(解凍してEclipseフォルダに上書き)

pydev01

Eclipseを実行し、Window-PreferenceからPyDevの設定。先ほどインストールしたpthon.exeのパスを設定

pydev02

File - New から Python Projectを選択し、プロジェクトを作成

pydev03 

コンテキストメニュー New - File から ファイルを作成 Hello.py

コードの補完が働く。

pydev04

ファイルを選択してコンテキストメニューから、Run As - Python Run で、実行すると、コンソールに結果が出力。

pydev05

めでたしめでたし。

-- 追記

Google App Engineを触ってみようと思い、Pythonを覚えようとしているんだけど、Pythonのいいところってなんなんだろなーとおもってぐぐったら、こんな記事が

こりゃPythonいい(笑)