Including files outside of the myapp directory tree

I’m trying to modify a server jinja template but I’m new to jinja and tornado.

Can I have the header.html file look for an image file that is outside of the myapp tree? In another directory called /images

My index.html file looks like this:

{% extends base %}

{% block contents %}

{% include "header.html" %}

{{ embed(roots.app) }}

{% endblock %}

where “app” is the name of my bokeh layout and header.html contains code looking for “/images/logo.png”

My directory tree looks like this:

/
|---images
|    +---logo.png
|---myapp
     |
     +---__init__.py
     |
     +---app_hooks.py
     +---data
     |    +---data.csv
     |
     +---main.py
     |---templates
     |    +---index.html
     |    +---header.html

The result is a page with the working bokeh app and text from the header, but the header logo image does not render. Console shows the error “404 GET /images/logo.png”

The built-in Bokeh server implemented by bokeh serve will not add routes for anything outside the app directory. I can think of two options:

  • Have some other site external to the Bokeh server host the content, and link to it with URL in your Bokeh app template
  • The Bokeh server can be used programmatically from APIs. Doing that you affords access to the underlying Tornado configuration, so you can add custom routes anwhere you like.
1 Like

Thank you! That first option sounds like it will work well for me, I’ll give it a try.

FYI the default Tornado routes are configured here:

bokeh/src/bokeh/server/urls.py at branch-3.7 · bokeh/bokeh · GitHub

So potentially you could modify, e.g. toplevel_routes (in place append) to include your new special route(s), before you start your own Server in your code.

Also just FYI for reference, the standard “startup” code for bokeh serve is here:

bokeh/src/bokeh/command/subcommands/serve.py at branch-3.7 · bokeh/bokeh · GitHub

1 Like

Oh interesting, thanks. Now I have something to think about.

1 Like

So I think the method of modifying toplevel_patterns in urls.py will probably work the best for me. But I’m confused about how the root is being handled in that script. From the description at the top, it looks like tornado will view “/” as “<prefix>/”

So if I fork urls.py and add (r"/…"), to toplevel_patterns: URLRoutes = [

Will it know how to handle that for navigating to /images or any other directory I might add?

Thank you

IIRC you would add an entry that maps a URL path like "/images" to a handler. In this case I think you’d want a Tornado StaticFileHandler configured to point to whatever arbitrary filesystem location you want to serve from.

1 Like

This worked, thank you!

I modified urls.py to:

#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Define standard endpoints and their associated views for a Bokeh Server
application.

These will be added by the :class:`~bokeh.server.tornado.BokehTornado`
application. The Bokeh Tornado application can then be served using Tornado's
built-in ``HTTPServer``.

.. data:: toplevel_patterns
    :annotation:

    Top-level routes, independent of any applications. They will be prefixed
    with any configured prefix.

    .. code-block:: python

        [
            ( r'/?',           RootHandler   ), # <prefix>/
            ( r'/static/(.*)', StaticHandler ), # <prefix>/static/
        ]

.. data:: per_app_patterns
    :annotation:

    Per-application routes. These be prefixed with the application path, as
    well as with any configured prefix.

    .. code-block:: python

        [
            ( r'/?',           DocHandler        ), # <prefix>/<app>/
            ( r'/ws',          WSHandler         ), # <prefix>/<app>/ws
            ( r'/metadata',    MetadataHandler   ), # <prefix>/<app>/metadata
            ( r'/autoload.js', AutoloadJsHandler ), # <prefix>/<app>/autoload.js
        ]

'''
# Please update the docstring above if any changes are made below

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
from typing import Any, TypeAlias

# External imports
from tornado.web import RequestHandler
from tornado.web import StaticFileHandler

# Bokeh imports
from ..embed.bundle import extension_dirs
from .views.autoload_js_handler import AutoloadJsHandler
from .views.doc_handler import DocHandler
from .views.metadata_handler import MetadataHandler
from .views.multi_root_static_handler import MultiRootStaticHandler
from .views.root_handler import RootHandler
from .views.static_handler import StaticHandler
from .views.ws import WSHandler

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'per_app_patterns',
    'toplevel_patterns',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

RouteContext: TypeAlias = dict[str, Any]

URLRoutes: TypeAlias = list[
    tuple[str, type[RequestHandler]] |
    tuple[str, type[RequestHandler], RouteContext],
]

toplevel_patterns: URLRoutes = [
    (r'/?', RootHandler),
    (r'/static/extensions/(.*)', MultiRootStaticHandler, dict(root=extension_dirs)),
    (r'/static/(.*)', StaticHandler),
    (r'/images/(.*)', StaticFileHandler, {"path": "/images"}),
]

per_app_patterns: URLRoutes = [
    (r'/?', DocHandler),
    (r'/ws', WSHandler),
    (r'/metadata', MetadataHandler),
    (r'/autoload.js', AutoloadJsHandler),
]

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
1 Like

I’m glad that worked out. IMO it wouldn’t be a huge lift to add some kind of hook to make it possible to extend the list of routes with standard bokeh serve. In case you are interested to propose this feature, and especially if you might be interested to work on a PR for it, please feel free to open a GitHub Issue.

1 Like