Ahh, are you using WSGIScriptAlias
to specify a directory instead of a file path? So it executes any .py
file in that directory, a bit like CGI scripts? If so, that's not how I've used mod_wsgi
in the past, I've always specified the path to an individual script file, which is used to serve all requests under that path.
This is more like how PA works, where you have a single entry-point for your whole domain. In my experience this is how most WSGI applications are set up, and many of the frameworks provide convenient request routing functionality so you can map URLs within your application rather than the WSGI middleware having to do it for you. Generally this makes your applications more portable since pretty much any WSGI hosting environment provides for a single entry-point, whereas the way of mapping URLs to many entry-points tends to vary quite a lot.
So yes, PA always executes the specified Python file. To do what you're hoping you'd need to have some sort of WSGI application which auto-loaded scripts from a directory yourself.
EDIT
For interest, I've had a quick stab at writing such a beast, using the Flask auto-generated WSGI file on PA as a starting point. Here's what I got:
"""Delegating WSGI application.
This application delegates the request to either a default application or one
of a set of "hook" scripts if they match the first path item.
"""
import errno
import imp
import os
import sys
import threading
import time
MIN_CHECK_TIME = 30
# Add your project and hooks directory to the path.
# (Leave initial entry alone on the assumption it's the current dir)
project_home = os.path.expanduser("~/mysite")
if project_home not in sys.path:
sys.path.insert(1, project_home)
hooks_home = os.path.join(project_home, "hooks")
if hooks_home not in sys.path:
sys.path.insert(2, hooks_home)
# Import "default" application to use in case no hooks apply.
from flask_app import app as default_application
class Hook(object):
"""Represents a hook script."""
def __init__(self, name, path):
self.name = name
self.path = path
self.last_used = 0
self.mtime = 0
self.app = None
def get_app_func(self):
"""Return app function or None if no longer valid.
This function checks whether the module needs to be reloaded, or
if it's been deleted. It returns a reference to the app() function
from the module if still valid, or None otherwise. It only checks
the file at most every MIN_CHECK_TIME seconds, returning the most
recent cached function reference in between these checks.
"""
if time.time() - self.last_used < MIN_CHECK_TIME and self.app:
return self.app
self.last_used = time.time()
try:
cur_mtime = os.stat(self.path).st_mtime
except OSError, e:
if e.errno != errno.ENOENT:
print >> sys.stderr, "error reading %s: %s" % (self.path, e)
return None
if cur_mtime <= self.mtime and self.app:
return self.app
try:
module = imp.load_source(self.name, self.path)
except Exception, e:
print >> sys.stderr, "error loading %s: %s" % (self.path, e)
return None
try:
self.app = module.app
except AttributeError:
print >> sys.stderr, "module %r has no app() func" % (self.name,)
return None
self.mtime = cur_mtime
return self.app
# Global variables for caching hooks.
hooks = {}
hook_lock = threading.Lock()
def get_hook(name):
"""Returns the application hook for specified app, or None."""
try:
with hook_lock:
hook = hooks.get(name, None)
if hook is None:
path = os.path.join(hooks_home, name + ".py")
if not os.path.exists(path):
return None
hook = Hook(name, path)
hooks[name] = hook
app = hook.get_app_func()
if app is None:
del hooks[name]
return app
except Exception, e:
print >> sys.stderr, "error loading %r: %s\n" % (name, e)
return None
def application(environ, start_response):
"""Main application wrapper - delegates to either default or hook app."""
path_items = environ.get("PATH_INFO", "").lstrip("/").split("/", 1)
app = None
if path_items and path_items[0]:
app = get_hook(path_items[0])
if app is not None:
environ["SCRIPT_NAME"] = "/" + path_items[0]
if path_items[1:]:
environ["PATH_INFO"] = "/" + "/".join(path_items[1:])
else:
environ["PATH_INFO"] = ""
app = default_application if app is None else app
return app(environ, start_response)
This implements the usual behaviour for the PA flask setup, which is to use the application object app
from the file flask_app.py
in ~/mysite
to serve the request. However, it also looks in ~/mysite/hooks
for Python files and if the top-level directory in the request URL matches any of those, it passes control to that instead (again it looks for an application called app
in the file - this could be a Flask app but I've tried to keep the code WSGI-clean so it should work with any framework).
It attempts to amend the SCRIPT_NAME
and PATH_INFO
environment so the URLs in the delegated Python file will be relative to the script. For example, if you have a file ~/mysite/hooks/example.py
and you make a request to http://username.pythonanywhere.com/example/foo/bar
then your SCRIPT_NAME
will be /example
and your PATH_INFO
will be /foo/bar
. In a Flask application, this means you'd route the request with:
@app.route('/foo/bar')
def foo_bar_handler():
# Code here
It should automatically reload any module which you've modified (it examines the last-modified time of the Python scripts on each request), but it has a hard-coded 10 second limit to prevent a high request rate from thrashing the filing system (i.e. it only checks for new and updated scripts at least 10 seconds after the last time it checked).
Bear in mind this is something pretty quick I've just whipped up, but hopefully it illustrates the idea. No, it's not the cleanest code in the world; yes, it needs more comments; yes, there are a number of efficiency improvements which could be made. But it's really just an example - you can modify it as suits your needs. To use it, go to your Web tab and look for the message "It is configured via a WSGI file stored at..." - that points you at the file you need to amend.
Warning: Unless you're pretty confident with your Python, I would suggest proceeding with caution. PA does a great job of insulating users from the internals of WSGI, but this script is pretty low-level and there's a risk you might spoil your working web app by messing around with these files. If you're pretty confident in how it all works then it's not too hard to get back, but I felt I should warn people - this isn't really aimed at total novice users.
EDIT 2: I've simplified the app a bit - I realised it was being needlessly inefficient by checking for all hook scripts instead of the one being requested. I did mention I'd whipped it up pretty quickly...
EDIT 3: Tidied it up a bit more, used a class to represent hook scripts. No longer working on something with long compile cycles, however, so that'll have to do for today... (^_^)