I spent the last week travailing away, trying to painfully find a way to deploy a web.py based API on IIS7 using PyISAPIe. As frustrations had begun to mount up, I had nearly decided to give up. Being a die-hard Linux and Mac guy, I despise having to work on Windows. Here I was, not only forced to work on Windows, but to find a solution for a problem that left no leaves unturned in its effort to drive me crazy. As if someone decided all this misery wasn’t quite enough, I had to work with a remote desktop session in order to research, tweak, bang my head, and get things to work. Eventually, I cut through massive frustration and despair, managing to find a satisfactory solution. I almost danced in excitment and relief, letting out all sorts of expletives directed at Windows in general and IIS in particular.
To get back to the important question of deploying a web.py
script on IIS7 using PyISAPIe, I will make it such that this guide will list down various steps I took, including snippets of relevant code I changed, to tame the beast. I can only hope that what is below will help a poor, miserable soul looking for help as I did (and found none).
I worked with PyISAPIe because I had successfully deployed multiple Django websites on IIS7 on it. The script in question was going to be a part of another Django website (though acting independently). It only made sense to use PyISAPIe for it as well.
First and foremost, I had to install the web.py
module on the system. Having had trouble before with IIS with web.py
installed through easy_install
, I decided to be safe and installed it from source.. Getting web.py
to work with PyISAPIe required a small hack (I notice I may make it sound as though it all came down to me in a dream, but in reality, it took me days to figure it out, and clearly after much anguish and pain). In the file Lib\site-packages\web\wsgi.py
lies the following function:
def _is_dev_mode(): # quick hack to check if the program is running in dev mode. if os.environ.has_key('SERVER_SOFTWARE') \ or os.environ.has_key('PHP_FCGI_CHILDREN') \ or 'fcgi' in sys.argv or 'fastcgi' in sys.argv \ or 'mod_wsgi' in sys.argv: return False return True
In its pristine state, when web.py
is imported from a source file through PyISAPIe, an exception is thrown. The exception, while I don’t have the exact message, is about it complaining about sys.argv
not having an attribute argv
, which reads fishy. Since the function _is_dev_mode()
only checks whether web.py
is being run in development mode, I thought I didn’t care about it since I wanted everything to run in production mode. I edited the function such that its body would be bypassed, while it returned a False
boolean value. It looked like this (the important changes I made are highlighted):
def _is_dev_mode(): return False # quick hack to check if the program is running in dev mode. if os.environ.has_key('SERVER_SOFTWARE') \ or os.environ.has_key('PHP_FCGI_CHILDREN') \ or 'fcgi' in sys.argv or 'fastcgi' in sys.argv \ or 'mod_wsgi' in sys.argv: return False return True
This innocuous little addition did away with the exception.
Next up, I used default Hello World-esque example of web.py
found on their site to test the deployment (of course, I went on to use my original API script, which was far too complex to trim down and fit into as an example). I called it code.py
(I placed it inside the folder C:\websites\myproject
). It looked like this:
import web urls = ( '/.*', 'hello', ) class hello: def GET(self): return "Hello, world." application = web.application(urls, globals()).wsgifunc()
It was pretty simple. You have to pay particular attention on the call to web.application
. I called the wsgifunc()
to return a WSGI-compatible function to boot the application. I prefer WSGI.
I set up a website under IIS using the IIS Management Console. Since I was working on a 64-bit server edition of Windows and had chosen to use 32-bit version of Python and all modules, I made sure to enable 32-bit support for the application pool being used for the website. This was important.
I decided to keep the PyISAPIe folder inside the folder where code.py
rested. This PyISAPIe folder contained, of import, the PyISAPIe.dll
file, and the Http
folder. Inside the Http
folder, I placed the most important file of all: the Isapi.py
. That file could be thought of as the starting point for each request that is made, what glues the Request to the proper Handler and code. I worked with the Examples\WSGI\Isapi.py
available as part of PyISAPIe. I tweaked the file to look like this:
from Http.WSGI import RunWSGI from Http import Env #from md5 import md5 from hashlib import md5 import imp import os import sys sys.path.append(r"C:\websites\myproject") from code import application ScriptHandlers = { "/api/": application, } def RunScript(Path): global ScriptHandlers try: # attempt to call an already-loaded request function. return ScriptHandlers[Path]() except KeyError: # uses the script path's md5 hash to ensure a unique # name - not the best way to do it, but it keeps # undesired characters out of the name that will # mess up the loading. Name = '__'+md5(Path).hexdigest().upper() ScriptHandlers[Path] = \ imp.load_source(Name, Env.SCRIPT_TRANSLATED).Request return ScriptHandlers[Path]() # URL prefixes to map to the roots of each application. Apps = { "/api/" : lambda P: RunWSGI(application), } # The main request handler. def Request(): # Might be better to do some caching here? Name = Env.SCRIPT_NAME # Apps might be better off as a tuple-of-tuples, # but for the sake of representation I leave it # as a dict. for App, Handler in Apps.items(): if Name.startswith(App): return Handler(Name) # Cause 500 error: there should be a 404 handler, eh? raise Exception, "Handler not found."
The important bits to note in the above code are the following:
- I import
application
from mycode
module. I set the PATH to include the directory in which the filecode.py
is so that theimport
statement does not complain. (I’ve to admit that the idea of importapplication
and feeding it intoRunWSGI
came to while I was in the loo.) - I defined a script handler which matches the URL prefix I want to associate with my
web.py
script. (In hindsight, this isn’t necessary, as theRunScript()
is not being used in this example). - In the
Apps
dictionary, I again route the URL prefix to thelambda
function which actually calls the `RunWSGI` function and feeds itapplication
. - I also imported the
md5
function from thehashlib
module instead of themd5
module as originally defined in the file. This was because Python complained aboutmd5
module being deprecated and suggested instead of usehashlib
.
And that’s pretty much it. It worked. I couldn’t believe what I saw on the browser in front of me. I danced around my room (while hurling all kinds of expletives).
There’s a caveat though. If you have specific URLs in your web.py
script, as I did in my API script, you will have to modify each of those URLs are add the /api/
prefix to them (or whatever URL prefix you set in the Isapi.py
. Without that, web.py
will not match any URLs in the file.
What a nightmare! I hope this guide serves to help others.
Thank you for reading. Good bye!
PS: If you want to avoid using PyISAPIe, there is a simpler way of deploying web.py on IIS. It is documented crudely over here.
Pingback: Guide: Deploying web.py on IIS7 using PyISAPIe | Tea Break
Brilliant! This reviewer says: a melancholy tale of one mans rise to sanity from the depths of Windows / IIS despair. Would recommend this movie to a friend.
Your article above states that you have created several Django websites on IIS 7. I have a substantial django/python application that is running under Apache/WSGI, I have been toying with getting it running under IIS7.5 on Windows 8-64bit. Under Apache the app runs in a 32bit virtualenv setup. When trying to run this same app under IIS 7.5 I am getting “404 not found”. Can you offer some advice?
Gary.