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.
# 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.
# GET /split
print("I'm cell #1")
I'm cell #1
# GET /split
print("I'm cell #2")
I'm cell #2
Reflection¶
The notebook runs nbconvert
on itself and serves the output HTML on /
.
# 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
.
# ResponseInfo GET /
import json
print(json.dumps({
"headers" : {
"Content-Type" : "text/html"
},
"status" : 200
}))
{"headers": {"Content-Type": "text/html"}, "status": 200}
Endpoints¶
# 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.
# 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.
rsvps = []
# GET /rsvps
import json
print(json.dumps(rsvps))
[]
# 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
# 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.
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='*'"]
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
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¶
# 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.