Jupyter compatibility

Use FastHTML in Jupyter notebooks
from httpx import get, AsyncClient

Helper functions


nb_serve

def nb_serve(
    app, log_level:str='error', port:int=8000, host:str='0.0.0.0', daemon:bool=False, **kwargs
):

Start a Jupyter compatible uvicorn server with ASGI app on port with log_level; use daemon=True for notebook tests so failed runs don’t block process shutdown


nb_serve_async

async def nb_serve_async(
    app, log_level:str='error', port:int=8000, host:str='0.0.0.0', **kwargs
):

Async version of nb_serve

Using FastHTML in Jupyter


show

def show(
    *s, iframe:bool=False, height:str='auto', style:NoneType=None
):

Same as fasthtml.components.show, but also adds htmx.process()


render_ft

def render_ft(
    **kw
):

Call once in a notebook or solveit dialog to auto-render components with HTMX support


htmx_config_port

def htmx_config_port(
    port:int=8000
):

JupyUvi

def JupyUvi(
    app, log_level:str='error', host:str='0.0.0.0', port:int=8000, start:bool=True, live:bool=False,
    live_rt:str='/_lr', daemon:bool=False, **kwargs
):

Start and stop a Jupyter compatible uvicorn server with ASGI app on port with log_level

Live Browser Refresh:

Since JupyUvi doesn’t restart the uvicorn process, and FastHTMLWithLiveReload relies on the websocket connection dropping to trigger a reload, we use a custom SSE implementation to add live client reloads to JupyUvi, controlled via the live param.

The logic flows as follows:

  • A version number is stored as a private attr on the instance: self._live_ver = 0
  • _setup_live() then sets up the app to handle browser refreshes:
    1. The live_rt = '/_lr' controls what endpoint the SSE generator will be attached to on the app. This is normalised w/ a fallback to ensure the endpoint works correctly.
    2. A Script tag is appended to the global app headers to ensure all standard routes connect to the SSE endpoint when opened in a client. It connects to the path self.live_rt and listens for a data: reload event from the SSE endpoint. On receiving this, a browser reload is triggered via navigation.reload().
    3. The route is registered on the app passed through to JupyUvi, and the generator (detailed below) is served from the EventStream response.
    4. A callback is attached to the Ipython event 'post_run_cell', which is triggered after a cell/ code has been executed in the kernel and returned. We incremement the JupyUvi version attribute each time a cell has been run to completion.
  • _live_sse is the generator served from our SSE endpoint, which does the following:
    1. Creates a reference to the global version number, since a generator is created fresh for each connected client.
    2. Create a while loop to poll the global version number -> generator’s version to check for code changes. self.server.should_exit is set via stop(), so all generators exit immediately when stop() is called so open connections don’t hang.
    3. If the client/generators version doesn’t match the instance version (code has been run in the kernel since the last loop iteration), we send a data: reload event to the EventSource listener and trigger a browser reload for each non-matching client.

Creating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook. Pass daemon=True for notebook tests if you want the process to exit cleanly even when a failing test never reaches server.stop().

app = FastHTML()
rt = app.route

@app.route
def index(): return 'hi'

port = 8000
server = JupyUvi(app, port=port)
get(f'http://localhost:{port}').text
'hi'

You can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application.

server.stop()

A good way to avoid spinning up two servers when running all the notebooks cells is to check whether the server variable is already defined, and stop if it is the case.

if 'server' in globals(): server.stop()
server.start()
server.stop()

JupyUviAsync

def JupyUviAsync(
    app, log_level:str='error', host:str='0.0.0.0', port:int=8000, **kwargs
):

Start and stop an async Jupyter compatible uvicorn server with ASGI app on port with log_level

server = JupyUviAsync(app, port=port)
await server.start()
async with AsyncClient() as client:
    r = await client.get(f'http://localhost:{port}')
print(r.text)
hi
await server.stop()

Using a notebook as a web app

You can also run an HTMX web app directly in a notebook. To make this work, you have to add the default FastHTML headers to the DOM of the notebook with show(*def_hdrs()). Additionally, you might find it convenient to use auto_id mode, in which the ID of an FT object is automatically generated if not provided.

fh_cfg['auto_id' ]=True

After importing fasthtml.jupyter and calling render_ft(), FT components render directly in the notebook.

show(*def_hdrs())
render_ft()

Handlers are written just like a regular web app:

server = JupyUvi(app, port=port)
(c := Div(''))
@rt
def hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true')

All the usual hx_* attributes can be used:

P('not loaded', hx_get=hoho, hx_trigger='load')

not loaded

FT components can be used directly both as id values and as hx_target values.

(c := Div(''))
@rt
def foo(): return Div('foo bar')
P('hi', hx_get=foo, hx_trigger='load', hx_target=c)

hi

server.stop()

Running apps in an IFrame

Using an IFrame can be a good idea to get complete isolation of the styles and scripts in an app. The HTMX function creates an auto-sizing IFrame for a web app.


HTMX

def HTMX(
    path:str='/', host:str='localhost', app:NoneType=None, port:int=8000, height:str='auto', link:bool=False,
    iframe:bool=True, client:NoneType=None
):

An iframe which displays the HTMX application in a notebook.

@rt
def index():
    return Div(
        P(A('Click me', hx_get=update, hx_target='#result')),
        P(A('No me!', hx_get=update, hx_target='#result')),
        Div(id='result'))

@rt
def update(): return Div(P('Hi!'),P('There!'))
server.start()
# Run the notebook locally to see the HTMX iframe in action
# Do not commit the outputs because it will break SolveIT
# HTMX()
server.stop()

ws_client

def ws_client(
    app, nm:str='', host:str='localhost', port:int=8000, ws_connect:str='/ws', frame:bool=True, link:bool=True,
    **kwargs
):