引き続き、「作ればわかる! Google App Engine for Java プログラミング」本にてPythonを修行中。
今回は「彼女からの目覚ましメール」について。
データストアやジョブ実行、タスクなどの重要なところに触れることができた。
■環境
- Windows7 x64
- Google App Engine SDK for Python 1.7.1 - 2012-08-21
■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の時刻処理(時計、タイムゾーン変換)
■Google App Engine で悩んだところと参考ページ
1. 公式情報
- メール:Google Developers - Google App Engine Mail Python API の概要
- データストアの型:Google Developers - Google App Engine 型とプロパティクラス
- スケジュール:Google Developers - Google App Engine Python 用クローンを使用したスケジュールされたタスク
- タスク:Google Developers - Google App Engine Task Queue Python API の概要
- タスク:Google Developers - Google App Engine タスクキュー 関数
- メールの受信:Google Developers - 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())
最初のうちは難しそうと思っていた内容も、手を動かしたら理解することができた。
引き続き、この調子で進めていきたい。