TL;DR: I created a small python package that allows you to share your jupyter notebooks right from the command line (like this one for example). To get started, visit nbhub.duarteocarmo.com. or pip install nbhub
.
We use jupyter notebooks for a wide range of things: quick analyses, serious projects, experimentations, and even for creating dashboards. However, when you want to share your work with someone that is not necessarily familiar with what a âjupyter notebookâ actually is, you run into some problems.
Letâs take a look at some of the options you have available when you want to share a notebook:
These are good options, but each one of them has drawbacks. In an ideal world, sharing a notebook should be as easy as sharing a link to a google doc, I envision something that:
Enter NbHub.
â ïžPreliminary note: NbHub is a proof of concept, or better, NbHub is a hack. It was developed in a weekend, has no guarantees of security or encryption, and is missing features like sharing notebooks with a password. For now, itâs a glorified self-hosted nbviewer alternative with a cli tool.
Letâs first see how it works from a userâs perspective, and then Iâll dive deeper into how it works.
To get started, simply install the command line tool using pip (even though you should be using something like pipx):
$ pip3 install nbhub
Now, letâs say I want to share the âAnalysis.ipynbâ notebook with someone. Simply run:
$ nbhub Analysis.ipynb
Youâll be walked through a simple command-line script that asks you a couple of questions. Once you are done, youâll receive a link such as this one. By clicking it, youâll see that your notebook is now on the web. You can just send that link to someone, and theyâll see your notebook.
Another thing that I love, is that if Iâm sharing a very large notebook, I can even link to a specific section of it. For example, click here to jump to the âcontextual relationshipsâ section of the example notebook.
NbHub consists of:
Letâs dive a bit deeper into these two:
The web server is a FastAPI app with 3 endpoints:
get("/")
: the homepage, or what you see when you visit nbhub.duarteocarmo.com. post(â/uploadâ)
: the endpoint that receives a post request with a notebook in the data field, converts it to .html, and then stores it in the server. get(â/notebook/idâ)
: the endpoint that exposes a stored notebook, so that the user can see it. You can visit the GitHub repo, or read the code below:
@app.get("/") # present homepage
async def home():
home_page = pathlib.Path("static/home.html").read_text()
return fastapi.responses.HTMLResponse(content=home_page, status_code=200)
@app.post("/upload") # endpoint that receives notebook, converts and stores.
async def respond(request: fastapi.Request):
unique_id = str(uuid.uuid4()).split("-")[0]
id_list = [item.stem for item in NOTEBOOK_STORAGE.glob("**/*.html")]
while unique_id in id_list:
unique_id = str(uuid.uuid4()).split("-")[0]
notebook_path = NOTEBOOK_STORAGE / f"{unique_id}.ipynb"
html_path = NOTEBOOK_STORAGE / f"{unique_id}.html"
try:
form = await request.form()
contents = await form[SITE_POST_LABEL].read()
notebook_json_keys = json.loads(contents).keys()
assert "nbformat" in notebook_json_keys
assert len(notebook_json_keys) == 4
assert sys.getsizeof(contents) / 1000000 < NOTEBOOK_SIZE_LIMIT
file = open(notebook_path, "wb")
file.write(contents)
file.close()
converter = subprocess.run(["jupyter", "nbconvert", notebook_path])
notebook_path.unlink()
return {
"status": "success",
"path": f"http://{SITENAME}/notebook/{html_path.stem}",
"password": "None",
"expiry date": "None",
}
except Exception as e:
print(str(e))
raise fastapi.HTTPException(status_code=404, detail="ERROR")
@app.get("/notebook/{notebook_id}") # serve notebook with particular ID
async def read_notebook(notebook_id: str):
file = pathlib.Path(NOTEBOOK_STORAGE / f"{notebook_id}.html")
if file.is_file():
file_contents = file.read_text()
return fastapi.responses.HTMLResponse(
content=file_contents, status_code=200
)
else:
raise fastapi.HTTPException(
status_code=404, detail="Notebook does not exist."
)
The command-line application is a Click application that emulates a post request with a given notebook. It also performs some user side checks.
You can visit the GitHub Repo, or read the main code below as well:
import click
import requests
POST_URL = "https://nbhub.duarteocarmo.com/upload"
SITE_POST_LABEL = "notebook-data"
SITE_URL = "https://nbhub.duarteocarmo.com"
@click.command()
@click.argument(
"notebook",
required=False,
type=click.Path(
exists=True,
file_okay=True,
dir_okay=True,
readable=True,
allow_dash=False,
),
)
def main(notebook):
"""Share notebooks from the command line.
NOTEBOOK is the jupyter notebook file you would like to share.
"""
check_notebook(notebook)
click.echo("\nWelcome to NbHub!")
click.echo(f"Consider supporting us at: {SITE_URL}\n")
click.echo(f"You are about to publish {notebook}\n")
click.confirm("Are you sure you want to publish it?", abort=True)
if click.confirm("Do you wish to set a password?"):
click.echo("")
click.echo(
f"Private notebooks are not available yet! đŹ, check {SITE_URL} for updates"
)
else:
assert status_ok(POST_URL) == True
files = {SITE_POST_LABEL: open(notebook, "rb")}
response = requests.post(POST_URL, files=files)
if response.status_code == 200:
link = response.json().get("path")
click.echo("")
click.echo("Published! âš")
click.echo(f"Visit your notebook at: {link}\n")
else:
click.echo("Sorry, something went wrong đ")
def check_notebook(notebook):
if not notebook:
click.echo("No notebook provided, nothing to do đ")
click.Context.exit(0)
def status_ok(url):
click.echo("\nQuerying the interwebs.. đ")
try:
requests.get(url)
except Exception:
click.echo("Sorry.. Our service appears to be down atm.")
click.Context.exit(0)
return False
return True
if __name__ == "__main__":
main()
My idea with NbHub was simply to build something that works for me. For the past 2 months, I have been using it every week with clients or co-workers in projects. Of course, I havenât been using it for sensitive information since the password feature is still missing.
In the main repo, I included a roadmap of features I would like to implement to make the tool even more awesome:
For now, Iâll keep using it as a hack, because this hack works, and hacks are awesome.
Get an email when I publish new posts. No spam. Ever.