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.