Archive for the ‘django’ tag
Push Notification and Python (Django)
I’ve been trying to get the iPhone Push notification services introduced in OS 3.0 working with Python (specifically, Django). It took a while but I figured I’d post my notes so it’ll spare someone the suffering I had to go through.
Other attempts at this (apns-python-wrapper (aka APNSWrapper) and Lee Packham’s code) both use the standard ssl libraries which I couldn’t get to work under Snow Leopard and Python 2.6.
I eventually got it going with Django and pyOpenSSL libraries. The easiest way is to get the prebuilt pyOpenSSL binaries from eGenix. If the test server is running on Snow Leopard the Python 2.6 UCS2 version seems to work best.
The thing about pyOpenSSL is that it expects the certificate and private key to be in separate .PEM files so you have to export each one from KeyChain Access in .P12 format then convert them to .PEM from the command line. For the development key it’s easier to specify the password during export then strip it out on the command line. In production you may not want to do that.
% openssl pkcs12 -clcerts -nokeys -out devcert.pem -in devcert.p12 % openssl pkcs12 -nocerts -out devkey.pem -in devkeypw.p12 % openssl rsa -in devkeypw.pem -out devkey.pemNow copy
devcert.pem and devkey.pem (with no password) into the server directory.
The following bit of code is in a file called PushSender.py and does the actual talking to the push server (I heavily edited out project-specific bits and standard exception and error-handling stuff. Hopefully didn’t bork it too badly
Also, in this example the actual communication runs in a thread. In some cases it might make more sense to run it as a main routine:
import os, sys
import struct, binascii, ssl, datetime
import threading
import simplejson as json
from socket import socket
from OpenSSL import SSL
class PushSender(threading.Thread):
def __init__(self, sandbox, token, message, badge, sound):
super(PushSender, self).__init__()
self.token = token
self.sandbox = sandbox
self.message = message
self.badge = badge
self.sound = sound
self.ctx = SSL.Context(SSL.SSLv3_METHOD)
if sandbox:
self.apnHost = "gateway.sandbox.push.apple.com"
self.ctx.use_certificate_file(os.path.join(PROJECT_ROOT, "devcert.pem"))
self.ctx.use_privatekey_file(os.path.join(PROJECT_ROOT, "devkey.pem"))
else:
self.apnHost = "gateway.push.apple.com"
self.ctx.use_certificate_file(os.path.join(PROJECT_ROOT, "prodcert.pem"))
self.ctx.use_privatekey_file(os.path.join(PROJECT_ROOT, "prodcert.pem"))
def run(self):
payload = {}
aps = {}
if (self.message):
aps["alert"] = str(self.message)
if (self.badge):
aps["badge"] = self.badge
if (self.sound):
aps["sound"] = str(self.sound)
payload["aps"] = aps
token = binascii.unhexlify(self.token)
payloadstr = json.dumps(payload, separators=(',',':'))
payloadLen = len(payloadstr)
fmt = "!cH32sH%ds" % payloadLen
command = '\x00'
msg = struct.pack(fmt, command, 32, token, payloadLen, payloadstr)
sock = socket()
s = SSL.Connection(self.ctx, sock)
s.connect((self.apnHost, 2195))
s.send(msg)
s.shutdown()
s.close()
To invoke it in a thread (the first param is True for sandbox and False for production server) from the main routine:
from PushSender import PushSender ... pushsender = PushSender(True, token, pushmessage, pushbadge, pushsound) pushsender.start()On the phone the token returned by the
didRegisterForRemoteNotificationsWithDeviceToken method is in the form <xxxxxxxx xxxxxxxx ...>. To pass it along to the server you have to strip out the <> and spaces. The unhexlify method then converts this string into a 32-byte hex binary value. The easiest way to strip out the extraneous stuff is to just do it on the client-side:
NSString *deviceToken = [[[[tokenString description]
stringByReplacingOccurrencesOfString:@"< " withString:@""]
stringByReplacingOccurrencesOfString:@">" withString:@""]
stringByReplacingOccurrencesOfString: @" " withString: @""];
One last thing: at least with the sandbox if the phone is running on WiFi sometimes push notices don’t come through. This may be due to router NAT or firewall configuration issues. One way to check is to go into XCode Organizer while the phone is tethered and run your app, then under the Organizer Console you can check for errors. To get around this you’ll want to turn off the WiFi on the phone and go with 3G. This usually makes push notices arrive as expected (and strangely enough the WiFi method goes back to working for a few minutes).
There’s more that needs to be done to make it production-ready but at least it’s good to know it’s doable:

Django bash shell shortcuts
SmileyChris had a post up recently on setting up bash aliases for Django. He uses the classic alias command which works for one-line shortcuts. I got inspired to put mine up too, but if you want to do something more elaborate, bash gives you this handy scripting language along with ‘shell commands’ so you can do something a bit more involved.
What I tried to do with these shortcuts was to create a set of mini-commands that quickly do what you need to do when developing Django. It might help someone out and squeeze an extra 2-3.5 microseconds each time you run a Django command. Hey, it all adds up.
In my case, I keep the directory for all my Django projects in a single ‘source’ directory so I can quickly jump in and out of them. The shell commands makes that assumption too. If your projects are all over the place, you’ll have to tweak things — but I’d suggest doing some housekeeping and moving everything to a common directory to help keep things neat and tidy.
To use these, copy the lines to the bottom of your ~/.bashrc or ~/.bash_profile. If you don’t see one of these in your home directory, remember that most *nix shells ignore files starting with a ‘.’ (period) so you’ll want to do something like:
% ls -al
To see all your files in the current directory. If you really don’t have one, then create a .bash_profile and put the following lines inside. You’ll want to customize the first four environment variables to point them to directories in your system. The ones there are samples, but once you set it up once, you’ll never have to worry about it again.
A minor caveat: These have been tested on a Mac OS-X Leopard. YM-will-most-likely-V. On with the code.
# Django shortcuts
# CHANGE: to directory where you installed Django sources.
DJANGOROOT=/dev/djangoproject
export DJANGOROOT
# CHANGE: to admin bin scripts directory under Django.
# This should work for the svn version.
DJANGOBIN=$DJANGOROOT/django/bin
export DJANGOBIN
PATH=$PATH:$DJANGOBIN:
export PATH
# CHANGE: to TCP port for testing.
# You go to http://localhost:8008 to hit the test sever.
DJANGOTESTPORT=8008
export DJANGOTESTPORT
# CHANGE: to base directory where you keep your Django projects.
DJANGOPROJECTBASE=~/Work/django
export DJANGOPROJCTBASE
# ---------------- Django shell commands ------------------
function django() {
echo "dadmin - Basic django-admin"
echo "dproj - set up (show) default django project"
echo "dsrc - jump to Django projects home directory"
echo "dls - list current projects in Django projects home directory"
echo "dcd - jump to current project directory"
echo "dstart - start a new project in the home directory"
echo "dapp - create a new application under the current project"
echo "dsync - Run syncdb and synchronize model with DBMS"
echo "drun - run current project in test server"
}
function dsrc() {
cd $DJANGOPROJECTBASE;
}
function dls() {
ls $DJANGOPROJECTBASE;
}
function dadmin() {
python django-admin.py $*;
}
function dstart() {
src;
python django-admin.py startproject $*;
}
function dproj() {
if [ $# -eq 0 ]
then
if [ $DJANGOCURRENTPROJECT ]
then
echo "Current project is: $DJANGOCURRENTPROJECT";
else
echo "Run 'dproj
fi
else
if [ -d $DJANGOPROJECTBASE/$1 ]
then
DJANGOCURRENTPROJECT=$1;
export DJANGOCURRENTPROJECT;
DJANGO_SETTINGS_MODULE="$DJANGOPROJECTBASE/$DJANGOCURRENTPROJECT";
export DJANGO_SETTINGS_MODULE;
echo "Current project set to: $1";
else
echo "Project directory '$1' doesn't exist."
fi
fi
}
function dcheck() {
if [ $DJANGOCURRENTPROJECT ]
then
cd $DJANGO_SETTINGS_MODULE;
return 0;
else
echo "Run 'dproj
return 1;
fi
}
function dapp() {
if [ dcheck ]
then
python manage.py startapp $*;
fi
}
function dcd() {
dcheck;
}
function dsync() {
if [ dcheck ]
then
python manage.py syncdb;
fi
}
function drun() {
if [ dcheck ]
then
python manage.py runserver $DJANGOTESTPORT$*;
fi
}
To get a reminder of how it all works, just type
% django
To see a list of current projects, use:
% dls
To start a new Django project, just go:
% dstart projectname
To set the current active project:
% dproj projectname
This checks to make sure the project exists under the base project directory. To quickly jump to the current project directory, you use:
% dcd
To create a new application under the current project use:
% dapp appname
Then you add the appname to your project’s settings.py file and you’re good to go. Once you’ve defined your model, run this to sync up the database and the model:
% dsync
And every time you want to run the test server for the current project, just run:
% drun
And assuming you didn’t change the DJANGOTESTPORT variable above, in the browser you go to:
http://localhost:8008/
or if localhost isn’t defined in your /etc/hosts file, you can also use:
http://127.0.0.1:8008
Under Windows, the easiest way to run this is to install Cygwin and then run bash from there.
Good luck.