Multi-page Panel app with custom templates - Need to run async code to get template variables

Hello!

I am building a multi-page app based on Bokeh/Panel and custom Jinja templates. Below is given a minimal example of the setup.

The problem I am trying to solve is to add variables to the template, where these variables must be retrieved from async functions.

Any ideas on how this can be achieved is much appreciated!

import panel as pn
from panel.io.server import get_server


def get_page():
    template = """
    {% extends base %}
    {% block contents %}
    {{ app_title }}
    <p>This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.</p>
    <br>
    <div style="display:table; width: 100%">
      <div style="display:table-cell; margin: auto">
        {{ embed(roots.A) }}
      </div>
      <div style="display:table-cell; margin: auto">
        {{ embed(roots.B) }}
      </div>
    </div>
    {% endblock %}
"""

    tmpl = pn.Template(template)

    tmpl.add_variable("app_title", "<h1>Custom Template App</h1>")

    tmpl.add_panel("A", pn.pane.Markdown("# Pane A"))
    tmpl.add_panel("B", pn.pane.Markdown("# Pane B"))

    # TODO: need to retrieve variables for the template from an async function:
    # If this was an async function we could do:
    # user_data = await get_user_data()
    # tmpl.add_variable("user_data", user_data)

    return tmpl


if __name__ == "__main__":
    server = get_server({"/page": get_page}, port=5006)
    server.start()
    server.io_loop.start()

1 Like

Hi @agrav

Welcome to the community and thanks for sharing your question with the community.

Can you use alex-sherman/unsync: Unsynchronize asyncio (github.com) as a workaround for now?

Can you load the variable using an async js script in the html template?

1 Like

Hi @Marc

Thanks a lot for the welcome and for your suggestions :slight_smile:

I tried the unsync package, but it did not work “out of the box” for my case.

I came up with a workaround where I patch the DocHandler class, similar to what is done in Panel, this allows me to insert my async code for getting the data I need to render the template. Below is a modified version of the example.

There are probably other ways, and it feels a bit hacky, but seems to work.

import asyncio

import panel as pn
from bokeh.server.urls import per_app_patterns
from bokeh.server.views.doc_handler import DocHandler as BkDocHandler
from panel.io import Resources
from panel.io.server import SessionPrefixHandler, server_html_page_for_session
from panel.io.server import get_server
from panel.io.state import state
from tornado.web import authenticated


async def get_user():
    await asyncio.sleep(.2)
    return "Me"


# Patch Bokeh DocHandler URL
class DocHandler(BkDocHandler, SessionPrefixHandler):

    @authenticated
    async def get(self, *args, **kwargs):
        with self._session_prefix():
            session = await self.get_session()
            state.curdoc = session.document

            # Get data for template rendering
            user = await get_user()

            try:
                resources = Resources.from_bokeh(self.application.resources())
                page = server_html_page_for_session(
                    session, resources=resources, title=session.document.title,
                    template=session.document.template,
                    template_variables={"user": user, **session.document.template_variables}
                )
            finally:
                state.curdoc = None
        self.set_header("Content-Type", 'text/html')
        self.write(page)


per_app_patterns[0] = (r'/?', DocHandler)


def get_page():
    template = """
    {% extends base %}
    {% block contents %}
    {{ app_title }}
    <p>The current user is {{ user }}</p>
    <br>
    <div style="display:table; width: 100%">
      <div style="display:table-cell; margin: auto">
        {{ embed(roots.A) }}
      </div>
      <div style="display:table-cell; margin: auto">
        {{ embed(roots.B) }}
      </div>
    </div>
    {% endblock %}
    """

    tmpl = pn.Template(template)

    tmpl.add_variable("app_title", "<h1>Custom Template App</h1>")

    tmpl.add_panel("A", pn.pane.Markdown("# Pane A"))
    tmpl.add_panel("B", pn.pane.Markdown("# Pane B"))

    return tmpl


if __name__ == "__main__":
    server = get_server({"/page": get_page}, port=5006)
    server.start()
    server.io_loop.start()
1 Like

Was wondering if instead of this approach you should not insert a component that can be displayed as empty, and then have an async callback that updates it, the callback being scheduled with pn.state.onload. And now I realize that onload doesn’t yet support async callbacks :upside_down_face:

2 Likes