from httpx import get, AsyncClientJupyter compatibility
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:- 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. - 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_rtand listens for adata: reloadevent from the SSE endpoint. On receiving this, a browser reload is triggered vianavigation.reload(). - The route is registered on the app passed through to
JupyUvi, and the generator (detailed below) is served from theEventStreamresponse. - 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 theJupyUviversion attribute each time a cell has been run to completion.
- The
_live_sseis the generator served from our SSE endpoint, which does the following:- Creates a reference to the global version number, since a generator is created fresh for each connected client.
- Create a
whileloop to poll the global version number -> generator’s version to check for code changes.self.server.should_exitis set viastop(), so all generators exit immediately whenstop()is called so open connections don’t hang. - 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: reloadevent to theEventSourcelistener 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' ]=TrueAfter 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
):