Getting Started#
Jupyrest is a library that converts Jupyter Notebooks into REST APIs. It turns notebooks into notebook functions.
This guide will demonstrate how to interact with a notebook function and other nice features that Jupyrest provides.
Prerequisites:#
Python 3.9 or later
Download the example project:
git clone https://github.com/microsoft/jupyrest.git
cd jupyrest/src/jupyrest_example
Set up the environment:
poetry install
poetry shell
pre-commit install
For here onwards, make sure you have run poetry shell
before running anything on the command line. This will ensure you are using the right Python virtual environment.
Starting the Web Server#
Run a Jupyrest HTTP server on port 5051:
python start_http.py 5051
Initializing a Jupyrest Client#
The JupyrestClient
is a handy wrapper over the Jupyrest HTTP API. The HTTP API OpenAPI spec can be found at the /docs
endpoint.
from jupyrest.client import JupyrestClient
from pprint import pprint
from IPython.display import HTML
import json
# initialize our client
base_url = "http://localhost:5051"
client = JupyrestClient(endpoint=base_url)
Below are some helper functions to better visualize the responses from the JupyrestClient
.
from IPython.display import HTML
import json
from fastapi.encoders import jsonable_encoder
# Some helper functions to aid with displaying data
# in this notebook.
def print_json(data):
print(json.dumps(jsonable_encoder(data), indent=4))
def display_response(response):
html_out = f"""
ExecutionId: <a href="{base_url}/api/notebook_executions/{response.execution_id}" target="_blank">{response.execution_id}</a> <br>
<a href="{base_url}{response.artifacts["html"]}" target="_blank">HTML</a> <br>
<a href="{base_url}{response.artifacts["html_report"]}" target="_blank">HTML Report</a><br>
<a href="{base_url}{response.artifacts["ipynb"]}" target="_blank">IPYNB</a><br>
"""
if response.has_output:
html_out += f'<a href="{base_url}{response.artifacts["output"]}" target="_blank">OUTPUT</a><br>'
if response.has_exception:
html_out += f'<a href="{base_url}{response.artifacts["exception"]}" target="_blank">EXCEPTION</a><br>'
display(HTML(html_out))
Getting the list of available Notebook Functions#
Based on what folder you have your notebooks in, Jupyrest will scan the directory and detect which notebooks in there are eligible notebook functions. This is done by looking for the *.config.json
files.
notebooks = await client.get_notebooks()
print_json(notebooks)
Get the details of a Notebook Function#
hello_world_notebook = await client.get_notebook(notebook_id="hello_world")
print_json(hello_world_notebook)
Executing a Notebook Function#
Ok, now to the fun stuff. We can use the execute_notebook_until_complete
function to do this.
First, lets try to execute a notebook with parameters that don’t match the expected schema:
await client.execute_notebook_until_complete(
notebook_id="hello_world",
parameters={
"foo": "bar"
}
)
Let’s execute the notebook with the correct parameters this time:
response = await client.execute_notebook_until_complete(
notebook_id="hello_world",
parameters={
"name": "PyCon 2024"
}
)
print_json(jsonable_encoder(response))
display_response(response)
Notebook Execution Artifacts#
Any successful execution of a notebook function will have the following artifacts:
output
(if any)the data passed into the
save_output()
function in the notebook, if called
html
an HTML view of the executed notebook
html_report
an HTML view with the code cells removed
ipynb
the .ipynb file of the executed notebook
The URLs for these artifacts are present in the response body (shown above). Of course, you can access these artifacts using the client as well:
execution_id = response.execution_id
html = await client.get_execution_html(
execution_id=execution_id
)
html_report = await client.get_execution_html(
execution_id=execution_id,
report_mode=True
)
ipynb = await client.get_execution_ipynb(
execution_id=execution_id
)
output = await client.get_execution_output(
execution_id=execution_id
)
Notebook Execution Output#
The most compelling artifact of a notebook execution is the output
. This is what makes a notebook into a notebook function.
Our "hello_world"
notebook is a notebook function because we can access its output:
print_json(output)
“Erroneous” Notebook Functions#
What happens when the code inside a notebook fails and throws an exception? Pretty much exactly what you’d expect!
We can debug any failed notebook execution by looking at its HTML. No need for any fancy logging or telemetry set ups.
from IPython.display import HTML
response = await client.execute_notebook_until_complete(notebook_id="error", parameters={})
print_json(jsonable_encoder(response))
display_response(response)
Sharing Input/Output Models#
A good practice in API is to factor commonly used schemas as models and reference them in many API endpoints. Jupyrest lets you do this too!
For this example, we will take a Portfolio Analysis notebook adapted from the Stock Analysis For Quant Github repository.
This notebook takes a Portfolio
object as input. Rather than define this as raw JSON schema, we can define this as a Python class:
The code below is only snippets, the full source code is in the jupyrest_example/
folder.
from jupyrest.nbschema import NbSchemaBase
from datetime import date
from typing import Dict
class Portfolio(NbSchemaBase):
start_date: date
end_date: date
holdings: Dict[str, float]
Now we can give this model a name when we create our Jupyrest application:
deps = InMemoryApplicationBuilder(
notebooks_dir=notebooks_dir,
models={
# we name our model here
"portfolio": Portfolio
}
).build()
With our API model created and named, we can reference it in our config.json
:
Portfolio_Analysis.config.json
{
"id": "portfolio_analysis",
"input": {
"type": "object",
"properties": {
"portfolio": {
"$ref": "nbschema://portfolio"
}
},
"required": [
"portfolio"
],
"additionalProperties": false
}
}
The cool thing is that when we use the API to get this notebook’s input/output schema, we don’t see the nbschema://
, everything is fully resolved into standard JSON schema:
portfolio_notebook = await client.get_notebook(notebook_id="portfolio_analysis")
print_json(portfolio_notebook)
Lets now execute our portfolio analysis notebook. We can give it a set of holdings and weights:
response = await client.execute_notebook_until_complete(
notebook_id="portfolio_analysis",
parameters={
"portfolio":{
"start_date": "2022-04-26",
"end_date": "2023-04-26",
"holdings": {
"AAPL": 0.5,
"MSFT": 0.2,
"AMD": 0.2,
"NVDA": 0.1
}
}
}
)
display_response(response)
Notice how the parameters have been converted into a Portfolio Python object in the notebook. Isn’t that just so cool!
The beauty is that our notebook has no idea that is being used as a REST API. It is all plain-old Python through and through.