GAEでPUSH通知時の"TypeError: must be _socket.socket, not socket"の対応
GAEでiosのPUSH通知(APNS)を実装しようとしたら、"TypeError: must be _socket.socket, not socket"のエラーがでたので、全体的な流れと対応をまとめる。
なお、このエラーは開発環境でのみ出る。
まずは、Google App EngineからiOSアプリへPush通知が送れるようになりました - laisoを参考に、下記2つを行った。
1、PyAPNsをインストール
2、下のソースの記述
from apns import APNs, Payload apns = APNs(use_sandbox=True, cert_file='cert.pem', key_file='pkey.pem') token_hex = '1c222886bcce84bd9fb34c274ea3d466be970b62c7d72582d6fc302f9b072503' # tokeh_hexはiOSアプリで取得するデバイストークン class APNsHandler(webapp.RequestHandler): def get(self): payload = Payload(alert="Hello World!", sound="default", badge=1) apns.gateway_server.send_notification(token_hex, payload) self.response.write(u"PUSH通知送信完了") app = webapp2.WSGIApplication([ ('/notify', APNsHandler) ], debug=True)
3、app.yamlに下記を追加
libraries:
- name: ssl
version: latest
で、開発環境のGAEを起動して、
http://localhost:8080/notify
にアクセスすると、
"TypeError: must be _socket.socket, not socket"
が発生する。
解決策は、ここに載っている。
やることは2つ。
1、"_ssl" and "_socket"を /path-to-gae-sdk/google/appengine/tools/devappserver2/python/sandbox.py の中の _WHITE_LIST_C_MODULESに追加。
2、/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/socket.py を、 /path-to-gae-sdk/google/appengine/dis27/socket.py にコピー(上書き)。
一番下のUITextFieldをキーボードで隠れないようにする
LINEのように、コメントを書き込む一番下のUITextFieldを、キーボード表示で隠れないようにする、簡単なやり方のメモ。
UIScrollViewの上にUITextFieldを配置してスクロールさせるサンプルはネットに散見されるけど、UIToolbarを使ったもっとシンプルなやり方がある。
ポイントは、
1、UIToolbarを使いその上にUITextFieldを配置する。
2、view.transformでキーボード分ずらす。
ソースコードは下記。
var myToolbar: UIToolbar! var myTextField: UITextField! override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) // ツールバーの追加 self.view.backgroundColor = UIColor.cyanColor() myToolbar = UIToolbar(frame: CGRectMake(0, self.view.frame.height - 44, self.view.frame.width, 44.0)) myToolbar.barStyle = UIBarStyle.BlackTranslucent myToolbar.tintColor = UIColor.whiteColor() myToolbar.backgroundColor = UIColor.blackColor() self.view.addSubview(myToolbar) // UITextFieldを作成する. myTextField = UITextField(frame: CGRectMake(0,0 ,self.view.frame.width, 44)) // 表示する文字を代入する. myTextField.placeholder = "コメントを書き込んでください" myTextField.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Center // 枠を表示する. myTextField.borderStyle = UITextBorderStyle.RoundedRect // Delegateを設定する. myTextField.delegate = self // ツールバーに追加する. myToolbar.addSubview(self.myTextField) // 通知設定 let notificationCenter = NSNotificationCenter.defaultCenter() notificationCenter.addObserver(self, selector: "handleKeyboardWillShowNotification:", name: UIKeyboardWillShowNotification, object: nil) notificationCenter.addObserver(self, selector: "handleKeyboardWillHideNotification:", name: UIKeyboardWillHideNotification, object: nil) } // UITextFieldをクリックした時。キーボードが出てくるので、その分だけずらす func handleKeyboardWillShowNotification(notification: NSNotification) { let userInfo = notification.userInfo! let keyboardScreenEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue() let transform = CGAffineTransformMakeTranslation(0, -keyboardScreenEndFrame.size.height); self.view.transform = transform } // UITextFieldでエンターを押した時。キーボードが消えるので、その分だけずらす func handleKeyboardWillHideNotification(notification: NSNotification) { self.view.transform = CGAffineTransformIdentity } // UITextFieldでリターンを押した時にキーボードを閉じる func textFieldShouldReturn(textField: UITextField) -> Bool { textField.resignFirstResponder() return true }
swiftでRealmをインストールして少し使った
CoreDataは少し複雑だし、iOSだけだし、もっと早いDBがあるということで、「Realm」を今後使うことにした。
インストール方法
もちろん手動ではなくて、cocoapodでRealmをインストールする。
1、Podfileを作成して以下を記述する。
platform :ios, "8.0"
use_frameworks!
pod 'Realm'
2、pod installの実行
pod install
3、RLMSupport.swiftをダウンロードして、プロジェクト内に配置する。
4、println(RLMRealm.defaultRealmPath())をソース内で実行して問題なければ、一応OK。
ソースコード
データ保存用のクラス作成。RDBのテーブルのようなもの。KVSでいうエンティティ。
class Book : RLMObject { dynamic var isbn = "" dynamic var name = "" dynamic var price = 0 }
保存して取り出す実験。
let realm = RLMRealm.defaultRealm() // Bookオブジェクト生成. let book = Book() book.isbn = "222" book.name = "book name sample" book.price = 100 // 保存 realm.transactionWithBlock() { realm.addObject(book) } for realmBook in Book.allObjects() { println("book name:\((realmBook as! Book).name)") }
保存、取り出しがログで確認できた。
rest_gaeでのユーザ削除とその関連データの削除
GAEでユーザ削除と、それに関連するデータ削除のケースを考える。
前提として、rest_gae+webapp2を使っている。
今回は「関連するデータ」を「投稿データ」とする。
ソースコードで表すと、下の例のように投稿データを表すUserPostクラスが、ownerとしてUserクラス(ユーザ)を持つ。
このUserPostは複数GAEのデータストア内に存在可能なので、結果として「ユーザ」と「投稿」の1対多の関係となる。
class UserPost(ndb.Model): owner = ndb.KeyProperty(kind='User') #投稿したUser description = ndb.StringProperty() #書き込み class RESTMeta: user_owner_property = 'owner'
【理想】
外部キーをカスケード設定して、ユーザ削除と同時に投稿データも自動削除されること
【現実】
上の理想は実現できない。。。RDBではないから。
【考えた対応策#1】
ユーザ削除後に呼ばれるコールバック関数を設定して、そのコールバック関数内で投稿データの削除を行う。そのためには、投稿データのparentをユーザにしておく必要がある。
<この対応策>:☓
<その理由>:他のモデルクラスの場合はコールバック関数が設定できるのに、Userモデルクラスに対してはコールバック関数が設定できなかった。
【考えた対応策#2】
利用者がユーザ削除ボタンを押した時(スマホアプリを想定)、アプリ側で
1,投稿データ削除
2,ユーザ削除
の順にAPI呼び出しを制御する。
<この対応策>:◯
<その理由>:ベストと言えないかもしれないが、他に解決策が見えなかったのでこれを採用する
GAE(python)のRESTfulなAPIのフレームワークのrest_gae
budowski/rest_gae · GitHubというGoogle App Engine上で動くpythonのRESTfulなWeb APIのフレームワークを使っているが、とても便利。
日本語での参考サイトがないので、メモとしてまとめる。
[概要]
Web APIなんて結局、jsonリクエストを受け取って、パラメータチェックして、レスポンスをjsonで返すだけのもの。
そのほとんどの機能をrest_gaeは提供する。インタフェースはREST。
1. ユーザ管理API
ユーザ作成、変更、削除などをのAPIがある。
1-1. 独自ユーザクラスの定義
from rest_gae.users import User class MyUser(User): """Our own user class""" prop1 = ndb.StringProperty(required=True) prop2 = ndb.StringProperty()
独自のユーザ情報(この場合は、prop1とprop2)が必要なければ、デフォルトで提供されているUserクラスを使えば良い。
Userクラスが持っているプロパティは、
・ユーザ名
・パスワード(ハッシュ化)
・adminユーザかどうか
・email認証済みかどうか
・作成日時と変更日時
※ドキュメントには書いてなかったので、ソースコードと実際に追加されたDataStoreから解析した!
そして、これを動かすには下記を記述する。
from rest_gae import * # This imports RESTHandler and the PERMISSION_ constants from rest_gae.users import UserRESTHandler # Make sure we initialize our WSGIApplication with this config (used for initializing webapp2_extras.sessions) config = {} config['webapp2_extras.sessions'] = { 'secret_key': 'my-super-secret-key', } app = webapp2.WSGIApplication([ UserRESTHandler( '/api/users', # The base URL for the user management endpoints user_model='models.MyUser', # Use our own custom User class user_details_permission=PERMISSION_LOGGED_IN_USER, verify_email_address=True, verification_email={ 'sender': 'John Doe <john@doe.com>', 'subject': 'Verify your email', 'body_text': 'Hello {{ user.full_name }}, click here: {{ verification_url }}', 'body_html': 'Hello {{ user.full_name }}, click <a href="{{ verification_url }}">here</a>' }, verification_successful_url='/verified-user', verification_failed_url='/verification-failed', reset_password_url='/reset-password', reset_password_email={ 'sender': 'John Doe <john@doe.com>', 'subject': 'Reset your password', 'body_text': 'Hello {{ user.name }}, click here: {{ verification_url }}', 'body_html': 'Hello {{ user.name }}, click <a href="{{ verification_url }}">here</a>' }, send_email_callback=my_send_email, allow_login_for_non_verified_email=False, user_policy_callback=lambda user, data: if len(data['password']) < 8: raise ValueError('Password too short') )], config=config)
UserRESTHandlerの中を見ていく。
'/api/users', # The base URL for the user management endpoints
URLを定義する。この場合、
http://ホスト名/api/users
でユーザ管理APIにアクセスすることになる。
user_model='models.MyUser', # Use our own custom User class
ユーザモデルの定義。先ほど作ったMyUserを使う。
user_details_permission=PERMISSION_LOGGED_IN_USER,
ユーザ情報を取得できるユーザの権限を設定している。ここでは、PERMISSION_LOGGED_IN_USERという(そのアカウントで)ログインしたユーザ以上に権限がある。
ユーザの権限には、下記がある。下に行くに従い、権限が強くなる。
・PERMISSION_ANYONE
・PERMISSION_LOGGED_IN_USER
・PERMISSION_OWNER_USER
・PERMISSION_ADMIN
その他の部分は、メール認証に関する機能で今回は無視する。
1-2. ユーザ管理APIの呼び出し
【実行サンプルあり】
A) POST /users : ユーザ情報の登録。誰でも可。
B) POST /users/login : ログイン。誰でも可。
C) GET /users/エンティティキー : あるユーザ情報の取得。前述の通りログインユーザ以上で可。
【実行サンプルなし】
GET /users : 全ユーザ情報の取得。adminユーザのみ可。
PUT /users/エンティティキー : ユーザ情報変更。パスワードも変更できる。ログインユーザ以上で可。
DELETE /users/エンティティキー : ユーザ情報削除。ログインユーザ以上で可。
【実行サンプルあり】のAPIを実際に呼び出してみる。
A) POST /users : ユーザ情報の登録。誰でも可。
email, user_name, passwordがUserクラスの必須のパラメータ。MyUserクラスでprop1を必須で定義したので、prop1も必須。
レスポンスでエンティティキーがidという名で返ってくる。
DataStore Viewerで中身をチェックすると、
ちゃんと値が入っている。
B) POST /users/login : ログイン。誰でも可。
れクエストはuser_nameとパスワードが必須。
レスポンスではuser_idが返される。これはMyUserエンティティのIDに一致する。つまり、ログインではMyUserエンティティキーは返却されない。
これがC) GET /users/エンティティキーの呼び出しで問題になる。エンティティキーがログインで取得できないので、下の2つのどちらかで対応する必要がある。
ア)アカウント作成時にローカルに保存(スマホアプリのみ可)
イ)エンティティキーを取得するAPIを別途用意する。
イ)の場合のサーバ側での処理は、
1.IDをクライアントから受け取る
2.ndb.Key(MyUser, IDの値).urlsafe()でエンティティキーを取得(※これがわからず苦労した)。
(例)ndb.Key(MyUser,4677872220372992).urlsafe()
3.クライアントに返却
ア)、イ)以外にもっと良いやり方があるかもしれないが、わからなかったのでこれで進めている。(誰か知っていたら教えて下さい。 )
※2015/4/18 追記
※ /users/me で自分のアカウントにアクセスできる(と分かった)ので、エンティティキーの取得は不要だった。
C) GET /users/エンティティキー : あるユーザ情報の取得。前述の通りログインユーザ以上で可。
取得成功。
Modelを追加してAPIを呼び出しもだいたい同じ感じ。次回書く。
[Mac] eclipse+pydevで作ったプロジェクトをGoogleAppEngineLauncherに登録する
eclipse+pydevとGoogleAppEngineLauncherとの連携にちょっと手間取ったので、メモとして残しておく。
事前条件
eclipse、pydev、GoogleAppEngineLauncherのインストールが必要。他のサイトを参考にインストールした。
やること
1.最初にeclipseでgoogle app engineのプロジェクト作成
2. 次にソースを少しいじる
3. 最後にGoogleAppEngineLauncherに登録
1.eclipseでgoogle app engineのプロジェクト作成
eclipseのメニューバーからファイル>新規>プロジェクトを選択 する。ウイザードが表示されるので、「PyDev Google App エンジン・プロジェクト」を選択する。
「次へ」をクリックすると、Pydevプロジェクトの設定画面が表示される。
プロジェクト名に適当な値を入力し(ここでは、test01とした)、インタープリターを「python」にする。また、「Add project directory to the PYTHONPATH」を選択する。
Google App エンジンのディレクトリを指定するように言われるので、下記を入力した。
/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine
すると、ライブラリ一覧が出てくるので、何も考えずに全て選択状態のままでOKをクリックする。
下の画面が出て来るので、次へをクリックする。
アプリケーションIDとテンプレート選択画面が出てくるので、ここではそれぞれ「ts-01」と「Hello Webapp World」を入力して完了をクリックして、プロジェクト作成完了!
※ アプリケーションIDとはgoogle app engineのアプリIDのこと
test01の下にhelloworld.pyがあれば作業成功。
2. 次にソースをいじる
helloworld.pyを開いて、「Hello, webapp World!」となっている部分を、「こんにちは〜」に変更する。
3. 最後にGoogleAppEngineLauncherに登録
GoogleAppEngineLauncherを起動して、左下の+をクリックする。
入力画面が出てくるので、Application IDは何も入力せず、Application Directoryに/Users/sky_env/eclipse-prj/workspace/test01を入力した。
IDがts-01のアプリが追加されていているのが確認できる。Runでアプリを起動してBrowserボタンで実行する。
「こんにちは〜」と表示されているのが確認できた(文字化けしていたので、ブラウザでUTF8にエンコードしたけど)。