Robot dispatching requests

Modular Flask Apps: A Guide to Application Dispatching

In our prior post, we showed how to create a basic Flask app (in our case to choose a restaurant). However, we have other apps that we want to create in addition to the restaurant picker. We also want to have a temperature app that collects temperatures from around the house and logs them in a database. While we could continue building this functionality into our restaurant picker app, I prefer a different approach. Instead, I want the apps to be modular with their routes separately maintained. This way, in the future, if I want to add or remove apps, it will be easy to do. To do this, we are going to use application dispatching.

Application Dispatching

Application Dispatching in Flask provides a way to run multiple Flask applications or other WSGI applications within a single Python process. This is useful for keeping different parts of the application separate. For instance, you can have separate front-end and back-end applications. Rather than combining all functionality into a single, monolithic application, each application maintains its own configuration and operates independently. The Werkzeug library’s DispatcherMiddleware is used to route requests to the appropriate application based on URL prefixes, subdomains, or paths. This allows for flexible and scalable application architectures. As an aside, the Werkzeug library is a collection of WSGI tools.

The code itself is not complex, but a few tricky parts were not immediately obvious (at least to me).

Code Repo Setup

The first step is setting up our code repository. Since I want to maintain the apps separately, I created a new parent repo called “FamilyApps”. Then I added the Restaurant Picker and Temperature Collector apps as submodules. 

    git submodule add <repository_url> [path]

This allows me to keep the development of these apps separate, while still easily bringing updates into my FamilyApps.

git submodule update –remote

Apache Configuration

Next, I created the configuration file for Apache.

<VirtualHost *:80>
   ServerName 192.168.1.201

   WSGIDaemonProcess familyapps user=www-data group=www-data threads=1 python-home=/var/www/FamilyApps/picker_venv
   WSGIScriptAlias / /var/www/FamilyApps/wsgi.py

   <Directory /var/www/FamilyApps>
       WSGIProcessGroup familyapps       
       WSGIApplicationGroup %{GLOBAL}
       Order deny,allow
       Allow from all
   </Directory>


   ErrorLog ${APACHE_LOG_DIR}/error.log
   CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

This mirrors the process we followed with the Restaurant Picker app. Remember to create the virtual environment (in this instance, I even reused the virtual environment name from the Restaurant Picker, picker_venv).

The Gateway

Next, we create our WSGI script that handles the routes. This is the heart of the dispatcher.

import sys
import os
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from flask import request


# Add your application's directory to the Python path
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__)+'/RestaurantPicker')
sys.path.append(os.path.dirname(__file__)+'/TempCollector')


import frontend
from RestaurantPicker import picker
from TempCollector import temp_app

application = DispatcherMiddleware(frontend.app, {
   '/restaurant': picker.restapp,
   '/temperature': temp_app.app
})

The first tricky part we encounter is appending the correct paths to the Python path so our dispatcher can locate the other applications. To do this, we use the following:

# Add your application's directory to the Python path
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__)+'/RestaurantPicker')
sys.path.append(os.path.dirname(__file__)+'/TempCollector')

This must be done before we try to import the other applications. Otherwise, you will encounter ‘file not found’ errors.

Finally, we create our application using DispatcherMiddleware.

class werkzeug.middleware.dispatcher.DispatcherMiddleware(app, mounts=None)

The app parameter defines what runs if the request does not match the routes specified in mounts. The mounts parameter is a dictionary of routes and applications.

In our case, we have:

application = DispatcherMiddleware(frontend.app, {
   '/restaurant': picker.restapp,
   '/temperature': temp_app.app
})

Frontend.app is a default application, which we will cover in a minute. ‘/restaurant’ is a route that points to our Restaurant Picker routes. We have a similar starting point for the Temperature Collector.

To access the restaurant picker routes, we only need to add ‘/restaurant’ to the beginning. For example, previously our route was http://192.168.1.201/restaurants to get a list of suggested restaurants. Now, our route is http://192.168.1.201/restaurant/restaurants.

The Frontend

The final piece is the frontend. This is where we go if no routes match either our ‘/restaurant’ or ‘/temperature’ routes. In our case, the frontend is very simple.

from flask import Flask, Response, request, render_template, jsonify

# Simple REST API - request restaurants and return 3 names
app = Flask(__name__)

@app.route('/', methods=['GET'])
def get_homepage():
   message = "Testing Flask"
   return render_template('index.html', message=message)

We render an index page with the message ‘Testing Flask’. Note that this file must be placed in a folder labeled ‘templates’ within the FamilyApp directory. Otherwise, you will receive a ‘page not found’ error.

Conclusion

That is all that is required to set up multiple apps using this method. 

Full disclosure, there is a much easier way to do this than I chose. Unfortunately, I didn’t think about this until after I had developed the approach I will cover here. If I could go back in time, I would have kept the apps completely separate and run them on their own ports. This would allow me to keep functionality completely separate. The only thing I would need to do to add a new app is add a new configuration file to the Apache2 sites-available folder/. In the future, I might make this change, but for now, I wanted to document what I did to run the apps on the same port.

However, if you want to avoid different port numbers and just use routes, this is a great way to run multiple Python applications in the same instance.

Leave a Reply

Your email address will not be published. Required fields are marked *