読者です 読者をやめる 読者になる 読者になる

「作ればわかる! Google App Engine for Java プログラミング」本をPythonで書いてみる (3)

GoogleAppEngine Python

引き続き、「作ればわかる! Google App Engine for Java プログラミング」本にてPythonを修行中。


今回は「彼女からの目覚ましメール」について。
データストアやジョブ実行、タスクなどの重要なところに触れることができた。

■環境

Python(+HTML)で悩んだところと参考ページ

1. セレクトボックスについて

Google App Engine のテンプレートと合わせて使うため、タグを改めて理解。

テンプレートを使った時のセレクトボックス値の与え方について少し悩んだが、

<select name="year">
    {% for year in years %}
        
        <!-- 現在年と同一の場合-->
        {% ifequal year selectedYear %}
        <option value={{ year }} selected>{{ year }}</option>
        
        <!-- その他の場合 -->
        {% else %}
        <option value={{ year }}>{{ year }}</option>
        
        {% endifequal %}
    {% endfor %}
</select>

のように、普通の変数っぽく使えばよかった模様。



2. 日付・時刻まわりの処理

テキストのJava同様、Pythonでもタイムゾーンを意識した処理をする必要がある。
ただ、JavaのCalendarクラスのようなものは見当たらず、別途導入すれば使えるようになる模様。


今回はそこまでの厳密な処理は不要であったため、以下を参考に、単純に9時間の加減で対応することとした。
python練習帳 - 日本時間に変換


また、データストアにはタイムゾーン情報を保存できないとのことだったので、以下と同様に扱いをUTCとした。
いしだ日記 - GAE/Pyでの日付型


日付・時刻処理の全体的な内容については以下を参考にした。
technical notes - Pythonの時刻処理(時計、タイムゾーン変換)



3. 日付の妥当性チェック

C#の「DateTime.TryParse()」みたいなものを探したが見つからなかったため、以下を参考に作成。
zuzuPost - 妥当な日付かどうかチェックする



Google App Engine で悩んだところと参考ページ

2. データストアから key_name で取得する

メソッド get_by_key_name を使えば良い
Google Developers - Google App Engine Modelクラス



3. データストアまわりのまとめ

以下がまとまっていて、わかりやすかった。
Pythonで遊ぶよ - Google App Engineのkey, id, name, kind, path, entity groupなどについてのまとめ



4. メールアドレスの妥当性チェック

厳密にやろうとすると非常に難しい模様。テキストに従い、今回は入力されていればOKとした。



4. メールアドレスを「名前」と「メールアドレス」に分離する方法

email.utils.parseaddr(address) を使えばOK。最初、標準モジュールでの存在に気づかなかった。
Python v2.7.3 documentation - 18.1.9. email.utils: Miscellaneous utilities



■フォルダ構成

css/
 +style.css
html/
 +error.html
 +index.html
 +index_post.html
img/
 +eri.jpg
__init__.py
alarm.py
app.yaml
cron.yaml
gae_util.py
index.py
receive.py
wakeup.py
wakeup_task.py

■ソース

GitHubにはchap4として追加。
今回はファイル量が多いため、pythonファイルのみ記載し、あとはGitHubにて。

index.py
# -*- coding: utf-8 -*-

import webapp2
import datetime
import os
from google.appengine.ext.webapp import template

import alarm
import gae_util

class IndexPage(webapp2.RequestHandler):
    def get(self):
        
        utcnow = datetime.datetime.utcnow()
        jstTime = gae_util.Utility.convert_jst_time(utcnow)
        
        if jstTime.hour >= 7:
            jstTime = jstTime + datetime.timedelta(days=1)

        self.response.out.write(template.render('html/index.html',{'selectedYear': jstTime.year,
                                                                   'selectedMonth': jstTime.month,
                                                                   'selectedDate': jstTime.day,
                                                                   'selectedHourOfDay': 7,
                                                                   'selectedMinute': 0,
                                                                   'years': self.get_years(),
                                                                   'months': self.get_months(),
                                                                   'dates': self.get_dates(),
                                                                   'hours': self.get_hours(),
                                                                   'minutes': self.get_minutes(),
                                                                   }))
        
        
    def post(self):
        
        email = self.request.get("email")
        nickname = self.request.get("nickname")
        year = self.request.get("year")
        month = self.request.get("month")
        date = self.request.get("date")
        hourOfDay = self.request.get("hourOfDay")
        minute = self.request.get("minute")
        
        
        # 入力チェック
        hasError = False
        emailError = False
        nicknameError = False
        wakeupDateError = False
        
        if email is None or email== '':
            emailError = True
            hasError = True
            
        if nickname is None or nickname == '':
            nicknameError = True
            hasError = True
            
        if not self.is_datetime(year, month, date):
            wakeupDateError = True
            hasError = True
        
        
        if hasError:
            # テンプレートの ifequal で正しく出力するには型を揃える必要があるため、int型へと変換しておく
            self.response.out.write(template.render('html/index.html',{'selectedYear': int(year),
                                                                       'selectedMonth': int(month),
                                                                       'selectedDate': int(date),
                                                                       'selectedHourOfDay': int(hourOfDay),
                                                                       'selectedMinute': int(minute),
                                                                       'years': self.get_years(),
                                                                       'months': self.get_months(),
                                                                       'dates': self.get_dates(),
                                                                       'hours': self.get_hours(),
                                                                       'minutes': self.get_minutes(),
                                                                       'email': email,
                                                                       'emailError': emailError,
                                                                       'nickname': nickname,
                                                                       'nicknameError': nicknameError,
                                                                       'wakeupDateError': wakeupDateError,
                                                                       }))
            return
              
        
        
        # データストアへの登録
        wakeupDate = datetime.datetime(int(year), int(month), int(date), int(hourOfDay), int(minute))
        
        #データストアの時刻は、タイムゾーンを考慮しないUTC時刻のため、タイムゾーンを加味して設定する
        jstWakeupDate = gae_util.Utility.convert_jst_time(wakeupDate)
        
        datastore = alarm.Alarm(key_name = email,
                      email = email,
                      nickname = nickname,
                      wakeupDate = jstWakeupDate,
                      count = 0)
        datastore.put()
        
        self.response.out.write(template.render('html/index_post.html',{'email': email,
                                                                        'nickname': nickname,
                                                                        'wakeupDate': wakeupDate,
                                                                        }))

    def get_years(self):
        #年は2年分
        utcnow = datetime.datetime.utcnow()
        years = []
        years.append(utcnow.year)
        years.append(utcnow.year + 1)
        
        return years
        
        
    def get_months(self):
        months = []
        
        for month in range(1, 13):
            months.append(month)
        
        return months
        
        
    def get_dates(self):
        dates = []
        
        for date in range(1, 32):
            dates.append(date)
            
        return dates
        
        
    def get_hours(self):
        hours = []
        
        for hour in range(0, 24):
            hours.append(hour)
        
        return hours
        
            
    def get_minutes(self):
        minutes = []
        
        for minute in range(0, 59, 5):
            minutes.append(minute)
            
        return minutes
    
    
    def is_datetime(self, year, month, date):
        try:
            datetime.date(int(year), int(month), int(date))
            return True
            
        except ValueError:
            return False
            
            
            
            
debug = os.environ.get('SERVER_SOFTWARE', '').startswith('Dev')
            
app = webapp2.WSGIApplication([('/index.html', IndexPage),
                               ('/', IndexPage)], debug=debug)
alarm.py
# -*- coding: utf-8 -*-

from google.appengine.ext import db

class Alarm(db.Model):
	#主キー:生成時に(key_name='emailアドレス')として指定してあげる
	email = db.StringProperty(required=True)
	
	nickname = db.StringProperty()
	wakeupDate = db.DateTimeProperty()
	count = db.IntegerProperty()
wakeup_task.py
# -*- coding: utf-8 -*-

import webapp2
import logging
import datetime
import os
from google.appengine.api import mail
from google.appengine.api.labs import taskqueue

import alarm
import gae_util


# メール送信クラス
# 制御クラスからメールアドレスがひとつ送られてくるため、そのアドレスに対して送信を行う

class WakeupTask(webapp2.RequestHandler):
    def post(self):
        
        # 例外が出た場合、そのタスクは終了する(再実行させない)
        try:
            self.update_alarm(self.request)
            
        except:
            logDatetime = gae_util.Utility.convert_jst_time(datetime.datetime.utcnow())
            logging.error(logDatetime.strftime(u'%Y/%m/%d %H:%M:%S'))
            
            
    def update_alarm(self, request):
        
        email = request.get("email")
        result = alarm.Alarm.get_by_key_name(email)
        nickname = result.nickname
        count = result.count
        
        
        if count == 0:
            result.count = count + 1
            result.put()
            
            # これが実行されるとタスクも消えてしまうため、タスクを再度登録する
            taskqueue.add(url='/task/wakeuptask', params={'email': email}, countdown=300)
            
        else:
            result.delete()
        
        
        self.send_mail(email, nickname, count)
        
        
    def send_mail(self, email, nickname, count):
        
        if count == 0:
            subject = u'時間だよーー'
            body = nickname + u'頼まれていた時間だよー' + u'\n' + u'予定があるんでしょ。早く準備してね。'
        else:
            subject = u'大変ー!'
            body = nickname + u'大変大変ー!' + '\n' + u'時間過ぎているよ!' + '\n' + u'早く早く!!'
        
        mail.send_mail(sender = 'eri@thinkamigaedemo.appspotmail.com',
                       to = email,
                       subject = subject,
                       body = body)
                       
                       
                       
debug = os.environ.get('SERVER_SOFTWARE', '').startswith('Dev')
                       
app = webapp2.WSGIApplication([('/task/wakeuptask', WakeupTask)], debug=debug)
wakeup.py
# -*- coding: utf-8 -*-

import webapp2
import datetime
import os
from google.appengine.api.labs import taskqueue

import alarm
import gae_util

        
class Wakeup(webapp2.RequestHandler):
    def get(self):
        results = alarm.Alarm.all()
        jstNow = gae_util.Utility.convert_jst_time(datetime.datetime.utcnow())
        
        results.filter("wakeupDate <= ", jstNow).order("wakeupDate")
        

        
        # 登録件数は多くない前提のため、イテレータにて全数取得する
        for result in results:
            taskqueue.add(url='/task/wakeuptask', params={'email': result.email})
            
            
            
debug = os.environ.get('SERVER_SOFTWARE', '').startswith('Dev')

app = webapp2.WSGIApplication([('/cron/wakeup', Wakeup)], debug=debug)
receive.py
# -*- coding: utf-8 -*-

import webapp2
import os
import email
from google.appengine.api import mail

import alarm


class Receive(webapp2.RequestHandler):
    def post(self):
        message = mail.InboundEmailMessage(self.request.body)
        
        #emailモジュールにて、名前とメールアドレスを分離
        address = email.utils.parseaddr(message.sender)
        result = alarm.Alarm.get_by_key_name(address[1])
        result.delete()
        
        
        
debug = os.environ.get('SERVER_SOFTWARE', '').startswith('Dev')

#以下の「<your application mail>」は、自分のアプリケーション用のメールアドレスに差し替える
app = webapp2.WSGIApplication([('/_ah/mail/<your application mail>', Receive)], debug=debug)
gae_util.py
# -*- coding: utf-8 -*-

import datetime

class Utility:

    @staticmethod
    def convert_jst_time(utcdatetime):
        return utcdatetime + datetime.timedelta(hours=9)
        
        
    @classmethod
    def get_jst_now(cls):
        return cls.convert_jst_time(datetime.datetime.utcnow())


最初のうちは難しそうと思っていた内容も、手を動かしたら理解することができた。
引き続き、この調子で進めていきたい。