I know that if I use panel serve --port 8080 --glob dashboards/*.py dashboards/**/*.py, all files will be picked up, and I can access these at http ://localhost:8080/page0.py , http ://localhost:8080/page1.py, etc. However the intermediate folder structure is lost.
Is there a way to serve these so that the URLs used to access would match the folder structure? For example http ://localhost:8080/page0.py, http ://localhost:8080/A/page1.py, and so on.
Sure. Here it is (not exact code as I don’t want to paste in anything proprietary to my work)
# script name is run.py
#
# calling without any arguments, will load everything in the main/ directory
# calling run_panel.py with a specific script name, that is in the main/ directory,
# For example if your script is main/level1/level2/script.py
# call this with
#
#python run.py level1/level2/script.py
#
# and it will run only that script. This allows for faster startup.
import logging
import os
import re
import sys
from typing import Dict, Iterator, List
# import first before importing anything from panel
from bokeh.command import util
from bokeh.application import Application
# this will extract level1/level2/script from the path
# main/level1/level2/script.py
APP_RE = re.compile("main/(.*)\.py")
def build_single_handler_applications(paths: List[str], argvs: Dict[str, List[str]] | None = None) -> Dict[str, Application]:
''' Return a dictionary mapping routes to Bokeh applications built using
single handlers, for specified files or directories.
This function iterates over ``paths`` and ``argvs`` and calls
:func:`~bokeh.command.util.build_single_handler_application` on each
to generate the mapping.
Args:
paths (seq[str]) : paths to files or directories for creating Bokeh
applications.
argvs (dict[str, list[str]], optional) : mapping of paths to command
line arguments to pass to the handler for each path
Returns:
dict[str, Application]
Raises:
RuntimeError
'''
applications: Dict[str, Application] = {}
argvs = argvs or {}
for path in paths:
application = util.build_single_handler_application(path, argvs.get(path, []))
# custom override for our purposes
# The original line is as below, which only returns the base name
# without the directory structure.
#route = application.handlers[0].url_path()
# our paths will all be main/**..../*.py
# so we need to find the bit between main and *.py
route = "/" + APP_RE.match(path).groups()[0]
if not route:
if '/' in applications:
raise RuntimeError("Don't know the URL path to use for %s" % (path))
route = '/'
applications[route] = application
return applications
# override the import here, so that our function is called instead
sys.modules['bokeh.command.util'].build_single_handler_applications = build_single_handler_applications
def main():
# panel imports must be done after the monkeypatch of bokeh.command.util
# so that our monkeypatched function will be taken instead
from panel.command import main
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
if len(sys.argv) == 1:
# The below is hardcoded to 4 levels because somewhere in the bokeh code,
# glob.glob is called without the recursive=True flag
# Monkeypatching like the above is not a good idea in this case,
# because glob.glob is a system installed library; monkeypatching
# would have too great a scope.
dashboards = [
"main/*.py",
"main/**/*.py",
"main/**/**/*.py",
"main/**/**/**/*.py",
"main/**/**/**/**/*.py",
]
elif len(sys.argv) == 2:
panels = [f"main/{sys.argv[1]}"]
else:
raise ValueError("Too many arguments to run.py")
port = os.environ.get("PORT", 8080)
sys.argv = [sys.argv[0], "serve", "--port", port, "--glob", "--autoreload"]
sys.argv += panels
logging.info(f"Starting with command line: {sys.argv}")
sys.exit(main())
if __name__ == '__main__':
main()