Generating Multiple Dash Instances

The problem

While building a webapp for text analysis and NLP things, I noticed that the project was getting bigger than expected, and having everything run in a single monolithic dash instance was too messy. Therefore, I started looking into how I could combine several dash instances, while still working on a single project and single api server. Needless to say, it was a bit tricky at times, and Dash requires you to do some silly things if you want to decouple and organize your project into several modules.

Multiple dash apps at the same time

The solutions seems simple at first sight:

	from flask import Flask

	server = Flask(__name__)

	dash_instance = dash.Dash(name, server=server, url_base_pathname="/dashboard/")

however, we still have the dash app in the same file, we may also want to have the callbacks in another

Expected Result

What I wanted, was to be able to generate a dash instance for every section of the platform, so, from the screenshot, each item in the menu would link to a different dash instance, all of them running in the same process however, so that this application could be easily deployable with a single Dockerfile.

img

From hardcoded mess to easily extensible product

As you can see from the code commented out, this module used to have all the dash instances basically hardcoded. By using a config file and importlib, however, we can drastically reduce the number of lines of code and easily extend our codebase to include more instances. I have also added the possibility to create template python files and needed directories when a new dash instance is added to the config, but its modules are not present.

	"""
	from home.home import app as home
	from home.home_layout import layout as home_layout
	from server.server import server
	from vowel_clustering.vowel_clustering import app as vwl_clst
	from vowel_clustering.vowel_clustering_layout import layout as vwl_clast_layout
	from word_frequency.word_frequency import app as wf
	from word_frequency.word_frequency_layout import base as wf_layout
	from chi_squared.chi_squared import app as chi_squared_app
	from chi_squared.chi_squared_layout import base as chi_squared_layout_var
	from global_components.utils import layout
	from text_classification.text_classification import app as text_classification_app
	from text_classification.text_classification_layout import base as text_classification_layout

	app = home
	app.layout = layout  # home_layout

	app1 = vwl_clst
	app1.layout = layout  # vwl_clst_layout

	app2 = wf
	app2.layout = layout  # layout  # wf_layout

	app3 = chi_squared_app
	app3.layout = layout  # chi_squared_layout_var

	app4 = text_classification_app
	app4.layout = layout
	"""

This is instead an approach that allows to add as many dash instances as you want, without having to worry about updating anything aside from a json config file with the name of the instance and endpoint path.

	import os
	from importlib import import_module

	from global_components.module_generator import ModuleGenerator
	from server.server import server
	from utils import load_module_configs

	apps = []
	modules = load_module_configs()
	existing_modules = [f.path[2:] for f in os.scandir() if f.is_dir() and f.path[2:] in modules]

	for module, endpoint in modules.items():
		if module not in existing_modules:
                 module_generator = ModuleGenerator(".", module)
                 module_generator.generate_new_module()
		dash_app = getattr(import_module(f"{module}.{module}"), "app")
		dash_app.layout = getattr(import_module(f"{module}.{module}_layout"), "layout")
		apps.append(dash_app)

	if __name__ == "__main__":
		server.run(debug=False, host="0.0.0.0", port=8080)

Main Flask server

The apps will set the server to this Flask instance in a dedicated module.

	"""
	This module creates the server instance.
	"""
	from flask import Flask

	server = Flask(__name__)

So we can now instanciate each dash app like the following, and have for each endpoint (url_base_pathname) a dedicated dash instance.

	from server.server import server
	
	def generate_app(name, url_base_pathname):
		return dash.Dash(name, server=server, external_stylesheets=[dbc.themes.LUX],
						 url_base_pathname=f"{url_base_pathname}", title="NLP Workspace")
	
	
	app = generate_app(__name__, url_base_pathname='/')

With the following structure we can organize our dash apps, each one in its own directory, with an app module, a dedicated module for callbacks and layout.

|
|- server
|    |
|    |- flask_server
|
|- app1
|    |
|    |- app_module
|    |- callback_module
|    |- layout_module

Since the structure is quite simple and clear, it is also easy to extend and add new ones in a programmatic way using simple config files.

Dynamically generating dash instances

With the following we then load on startup, from the main module, all the dash apps, and also create the app_module, callback_module and layout_module templates of the ones included in the json config file but not yet created. To do this we will use a class whose job is to generate the directory and template files.

	modules = load_module_configs()
	existing_modules = [f.path[2:] for f in os.scandir() if f.is_dir() and f.path[2:] in modules]

	for module, endpoint in modules.items():
		if module not in existing_modules:
		        module_generator = ModuleGenerator(".", module)
		        module_generator.generate_new_module()
		dash_app = getattr(import_module(f"{module}.{module}"), "app")
		dash_app.layout = getattr(import_module(f"{module}.{module}_layout"), "layout")
		apps.append(dash_app)

Module generator class

	class ModuleGenerator:

		def __init__(self, pwd, module_name):
                self.pwd = pwd
				self.module_name = module_name
				# TODO: Check module name only contains alpha and underscores
				self.files_to_generate = [f"{self.module_name}/__init__.py",
									  f"{self.module_name}/{self.module_name}.py",
									  f"{self.module_name}/{self.module_name}_layout.py"]

		def _create_new_module_files(self):
				logger.info("Generating new module directory and files...")
				os.mkdir(self.module_name)
				for file in self.files_to_generate:
				    with open(file, "w") as module_file:
				    	 	module_file.write("")

		def _fill_main_module(self):
				with open(self.files_to_generate[1], "w") as file:
						file.write(main_module_code % self.module_name)

		def _fill_layout_module(self):
				with open(self.files_to_generate[2], "w") as file:
						file.write(layout_module_code % (self.module_name, self.module_name, self.module_name))

		def generate_new_module(self):
				self._create_new_module_files()
				logger.info("Creating templates for new module files...")
				self._fill_main_module()
				self._fill_layout_module()
				logger.info("New dash module created")

Lessons learned

Dash has some behaviors that do not play nicely with some of the tools from IDE’s such as PyCharm. For example, if you decide to use the same structure and have the app, callbacks and layout in different files, you need to import the callbacks module (even if you do not use anything from it) into the layout module. This, however will make the import line be highlighted as a useless import by PyCharm, and if you format the file automatically with Ctrl+Alt+L, you will lose the import and the callbacks will not work.

To check how all this was implemented in a project, check out this repository https://github.com/andcarnivorous/NLP-workspace-dashapp