Hello, notebook-http mode¶

A notebook serving itself and an API from fly.io using Jupyter Kernel Gateway notebook-http mode.

Alternative title: You might not need Flask.

Max Bo, published 25 September 2024

github.com/MaxwellBo/hello-notebook-http-mode


I was reading the new htmx HATEOS article, and started wondering how easy it would be to create a self-documenting API.

Without further ado:

A refresher on notebook-http mode¶

Prefix a notebook a single line comment to turn it into a HTTP handler.

Visit /hello/world to see this in action.

In [60]:
# GET /hello/world
print("hello world")
hello world

Multiple cells may share the same annotation. Their content is concatenated to form a single code segment at runtime. This facilitates typical, iterative development in notebooks with lots of short, separate cells: The notebook author does not need to merge all of the cells into one, or refactor to use functions.

/split

In [61]:
# GET /split
print("I'm cell #1")
I'm cell #1
In [62]:
# GET /split
print("I'm cell #2")
I'm cell #2

Reflection¶

The notebook runs nbconvert on itself and serves the output HTML on /.

In [68]:
# GET /
import os
import subprocess

if not os.path.exists('hello-notebook-http-mode.html'):
    subprocess.run(["jupyter", "nbconvert", "--to", "html", "hello-notebook-http-mode.ipynb"])

with open('hello-notebook-http-mode.html', 'r') as file:
    print(file.read())

We have to use this somewhat goofy ResponseInfo "metadata companion cell" to force the Content-Type to text/html.

In [64]:
# ResponseInfo GET /
import json

print(json.dumps({
    "headers" : {
        "Content-Type" : "text/html"
    },
    "status" : 200
}))
{"headers": {"Content-Type": "text/html"}, "status": 200}

Endpoints¶

Basic GET¶

We can return on-demand dynamic data from an endpoint.

This will be different from the time displayed below, which is the time interred into the .ipynb file when it was last evaluated and saved.

/time

In [65]:
# GET /time

from datetime import datetime

print(datetime.now())
2024-09-25 15:10:01.646743

Path and query parameters¶

Path and query parameters are injected into a REQUEST object. REQUEST is not available at standard Jupyter notebook evaluation time, hence the Big Red Error.

/users/mb/collections/guitars?limit=5

In [66]:
# GET /users/:userId/collections/:collectionId
import json

req = json.loads(REQUEST)
print(req)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[66], line 4
      1 # GET /users/:userId/collections/:collectionId
      2 import json
----> 4 req = json.loads(REQUEST)
      5 print(req)

NameError: name 'REQUEST' is not defined

POST and forms¶

We can also handle POST requests from forms.

<script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>
<form hx-post="/rsvps" hx-target="#result" hx-swap="afterend">
  <input type="text" name="name" placeholder="Your name">
  <input type="submit" value="RSVP">
</form>
<div id="result" hx-get="/rsvps" hx-trigger="load" hx-swap="innerHTML"></div>

Refresh the page to see the RSVPs persisted (temporarily) serverside.

In [37]:
rsvps = []

/rsvps

In [22]:
# GET /rsvps
import json

print(json.dumps(rsvps))
[]
In [38]:
# POST /rsvps
import json

req = json.loads(REQUEST)
rsvps.append(req["body"]["name"][0])
print(json.dumps(rsvps))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[38], line 4
      1 # POST /rsvps
      2 import json
----> 4 req = json.loads(REQUEST)
      5 rsvps.append(req["body"]["name"][0])
      6 print(json.dumps(rsvps))

NameError: name 'REQUEST' is not defined
In [21]:
# ResponseInfo POST /rsvps
print(json.dumps({
    "headers" : {
        "Content-Type" : "application/json"
    },
    "status" : 201
}))
{"headers": {"Content-Type": "application/json"}, "status": 201}

Swagger¶

The Kernel Gateway auto-generates API docs served at /_api/spec/swagger.json.

Docker¶

This is the Docker configuration I adapted from Jupyter Kernel Gateway documentation - Running using a Docker stacks image.

In [2]:
with open('Dockerfile', 'r') as file:
    print(file.read())
FROM jupyter/datascience-notebook

RUN pip install jupyter_kernel_gateway

WORKDIR /app

ADD . /app

# run kernel gateway on container start, not notebook server
EXPOSE 8888

CMD ["jupyter", "kernelgateway",  "--KernelGatewayApp.api='kernel_gateway.notebook_http'", "--KernelGatewayApp.ip=0.0.0.0", "--KernelGatewayApp.port=8888", "--KernelGatewayApp.seed_uri=hello-notebook-http-mode.ipynb", "--KernelGatewayApp.allow_origin='*'"]

In [67]:
with open('.dockerignore', 'r') as file:
    print(file.read())
hello-notebook-http-mode.html

And then to run locally, I use:

docker build -t my/kernel-gateway .
docker run -it --rm -p 8888:8888 my/kernel-gateway

fly.io¶

To deploy to fly.io, run

fly launch

which generates something like:

In [24]:
with open('fly.toml', 'r') as file:
    print(file.read())
# fly.toml app configuration file generated for hello-notebook-http-mode on 2024-09-25T09:29:23+10:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'hello-notebook-http-mode'
primary_region = 'syd'

[build]

[http_service]
  internal_port = 8888
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

See also¶

I love notebooks. I have also written:

  • Reactive HTML notebooks
  • @celine/celine, a microlibrary for building reactive HTML notebooks

License¶

In [25]:
# GET /LICENSE
with open('LICENSE', 'r') as file:
    print(file.read())
MIT License

Copyright (c) 2024 Max Bo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.