PasteDeploy is a great tool for managing WSGI applications. Unfortunately,
there is no support of configuration formats other than INI-files. Montague
is going to solve the problem, but its documentation is unfinished and says
nothing useful. Hope, it will be changed soon. But if you don’t want to wait,
as me do, the following recipe is for you.
Using ConfigTree on my current project, I stumbled with the problem: how to
serve Pyramid applications (I got three ones) from the custom configuration?
Here is how it looks like in YAML:
app:
use: "egg:MyApp#main"
# Application local settings goes here
filters:
-
use: "egg:MyFilter#filter1"
# Filter local settings goes here
-
use: "egg:MyFilter#filter2"
server:
use: "egg:MyServer#main"
# Server local settings goes here
The easy way is to build INI-file and use it. The hard way is to make my own
loader. I chose the hard one.
PasteDeploy provides public functions loadapp, loadfilter, and loadserver.
However, these functions don’t work, because they don’t accept local settings. Only global
configuration can be passed into.
app = loadapp('egg:MyApp#main', global_conf=config)
But the most of PasteDeploy-based applications simply ignore global_conf.
For example, here is the paste factory of Waitress:
def serve_paste(app, global_conf, **kw):
serve(app, **kw) # global_conf? Who needs this shit?
return 0
I dug around the sources of PasteDeploy and found loadcontext function.
It is kind of low level private function. But who cares? So here is the
source of loader, that uses the function.
from paste.deploy.loadwsgi import loadcontext, APP, FILTER, SERVER
def run(config):
def load_object(object_type, conf):
conf = conf.copy()
spec = conf.pop('use')
context = loadcontext(object_type, spec) # Loading object
context.local_conf = conf # Passing local settings
return context.create()
app = load_object(APP, config['app'])
if 'filters' in config:
for filter_conf in config['filters']:
filter_app = load_object(FILTER, filter_conf)
app = filter_app(app)
server = load_object(SERVER, config['server'])
server(app)
But it is not the end. Pyramid comes with its own command pserve, that
uses PasteDeploy to load and start up application from INI-file.
And there is an option of the command that makes development fun.
I mean --reload one. It starts separate process with a file monitor that
restarts your application when its sources are changed. The following code
provides the feature. It depends on Pyramid, because I don’t want to reinvent
the wheel. But if you use another framework, it won’t be hard to write your
own file monitor.
import sys
import os
import signal
from subprocess import Popen
from paste.deploy.loadwsgi import loadcontext, APP, FILTER, SERVER
from pyramid.scripts.pserve import install_reloader, kill
def run(config, with_reloader=False):
def load_object(object_type, conf):
conf = conf.copy()
spec = conf.pop('use')
context = loadcontext(object_type, spec)
context.local_conf = conf
return context.create()
def run_server():
app = load_object(APP, config['app'])
if 'filters' in config:
for filter_conf in config['filters']:
filter_app = load_object(FILTER, filter_conf)
app = filter_app(app)
server = load_object(SERVER, config['server'])
server(app)
if not with_reloader:
run_server()
elif os.environ.get('master_process_is_running'):
# Pass your configuration files here using ``extra_files`` argument
install_reloader(extra_files=None)
run_server()
else:
print("Starting subprocess with file monitor")
environ = os.environ.copy()
environ['master_process_is_running'] = 'true'
childproc = None
try:
while True:
try:
childproc = Popen(sys.argv, env=environ)
exitcode = childproc.wait()
childproc = None
if exitcode != 3:
return exitcode
finally:
if childproc is not None:
try:
kill(childproc.pid, signal.SIGTERM)
except (OSError, IOError):
pass
except KeyboardInterrupt:
pass
That’s it. Wrap the code with a console script and don’t forget to initialize
the logging.