Skip to content

Commit

Permalink
Add more documentation (#8)
Browse files Browse the repository at this point in the history
Return outputs from `execute_cell`

Co-authored-by: Frédéric Collonval <[email protected]>
  • Loading branch information
fcollonval and fcollonval authored Dec 4, 2024
1 parent 5f50c6e commit aae60dc
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 17 deletions.
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

Client to interact with Jupyter notebook model.

## Requirements

- Jupyter Server

## Install

To install the extension, execute:
Expand All @@ -16,6 +12,62 @@ To install the extension, execute:
pip install jupyter_nbmodel_client
```

## Usage

1. Ensure you have an environment with `jupyter-server-ydoc` installed.

> To reproduce the above video you will need to install `jupyterlab`, `jupyter-collaboration` and `scikit-learn` and `matplotlib` for the notebook demo.
1. Start the server `jupyter server` (or JupyterLab like in the video)

1. Write down the URL (usually `http://localhost:8888`) and the server token

1. Open a Python terminal

1. Execute the following snippet to add a cell

```py
from jupyter_nbmodel_client import NbModelClient

with NbModelClient(server_url="http://localhost:8888", token="...", path="test.ipynb") as notebook:
notebook.add_code_cell("print('hello world')")
```

1. Another example adding a cell and executing within a kernel process

```py
from jupyter_kernel_client import KernelClient
from jupyter_nbmodel_client import NbModelClient

with KernelClient(server_url="http://localhost:8888", token="...") as kernel:
with NbModelClient(server_url="http://localhost:8888", token="...", path="test.ipynb") as notebook:
cell_index = notebook.add_code_cell("print('hello world')")
results = notebook.execute_cell(cell_index, kernel)

assert results["status"] == "ok"
assert len(results["outputs"]) > 0
```

> [!NOTE]
> Instead of using the clients as context manager, you can call the ``start()`` and ``stop()`` methods.
```py
from jupyter_nbmodel_client import NbModelClient

kernel = KernelClient(server_url="http://localhost:8888", token="...")
kernel.start()
try:
notebook = NbModelClient(server_url="http://localhost:8888", token="...", path="test.ipynb"):
notebook.start()
try:
cell_index = notebook.add_code_cell("print('hello world')")
results = notebook.execute_cell(cell_index, kernel)
finally:
notebook.stop()
finally:
kernel.stop()
```

## Uninstall

To remove the extension, execute:
Expand Down
7 changes: 4 additions & 3 deletions jupyter_nbmodel_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Client to interact with Jupyter notebook model."""

from .client import NbModelClient
from .model import NotebookModel
from nbformat import NotebookNode

from .client import NbModelClient
from .model import KernelClient, NotebookModel

__version__ = "0.1.0"

__all__ = ["NbModelClient", "NotebookModel", "NotebookNode"]
__all__ = ["KernelClient", "NbModelClient", "NotebookModel", "NotebookNode"]
10 changes: 9 additions & 1 deletion jupyter_nbmodel_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@


class NbModelClient(NotebookModel):
"""Client to one Jupyter notebook model."""
"""Client to one Jupyter notebook model.
Args:
server_url: Jupyter Server URL
token: Jupyter Server authentication token
path: Notebook path relative to the server root directory
timeout: Request timeout in seconds; default to environment variable REQUEST_TIMEOUT
log: Custom logger
"""

def __init__(
self,
Expand Down
144 changes: 135 additions & 9 deletions jupyter_nbmodel_client/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,83 @@
current_api = versions[current_nbformat]


def output_hook(ycell: pycrdt.Map, msg: dict) -> None:
class KernelClient(t.Protocol):
def execute_interactive(
self,
code: str,
silent: bool = False,
store_history: bool = True,
user_expressions: dict[str, t.Any] | None = None,
allow_stdin: bool | None = None,
stop_on_error: bool = True,
timeout: float | None = None,
output_hook: t.Callable | None = None,
stdin_hook: t.Callable | None = None,
) -> dict[str, t.Any]:
"""Execute code in the kernel with low-level API
Output will be redisplayed, and stdin prompts will be relayed as well.
You can pass a custom output_hook callable that will be called
with every IOPub message that is produced instead of the default redisplay.
Parameters
----------
code : str
A string of code in the kernel's language.
silent : bool, optional (default False)
If set, the kernel will execute the code as quietly possible, and
will force store_history to be False.
store_history : bool, optional (default True)
If set, the kernel will store command history. This is forced
to be False if silent is True.
user_expressions : dict, optional
A dict mapping names to expressions to be evaluated in the user's
dict. The expression values are returned as strings formatted using
:func:`repr`.
allow_stdin : bool, optional (default self.allow_stdin)
Flag for whether the kernel can send stdin requests to frontends.
stop_on_error: bool, optional (default True)
Flag whether to abort the execution queue, if an exception is encountered.
timeout: float or None (default: None)
Timeout to use when waiting for a reply
output_hook: callable(msg)
Function to be called with output messages.
If not specified, output will be redisplayed.
stdin_hook: callable(msg)
Function to be called with stdin_request messages.
If not specified, input/getpass will be called.
Returns
-------
reply: dict
The reply message for this request
"""
...


def output_hook(outputs: list[dict], ycell: pycrdt.Map, msg: dict) -> None:
"""Callback on execution request when an output is emitted.
Args:
outputs: A list of previously emitted outputs
ycell: The cell being executed
msg: The output message
"""
# FIXME converge with output_hook in KernelClient
msg_type = msg["header"]["msg_type"]
if msg_type in ("display_data", "stream", "execute_result", "error"):
# FIXME support for version
output = nbformat.v4.output_from_msg(msg)
outputs.append(output)

if ycell is not None:
cell_outputs = ycell["outputs"]
Expand All @@ -46,7 +111,10 @@ def output_hook(ycell: pycrdt.Map, msg: dict) -> None:
cell_outputs.append(output)

elif msg_type == "clear_output":
# FIXME msg.content.wait - if true should clear at the next message
# msg.content.wait is ignored - if true should clear at the next message
# FIXME this is to fake some animation do we care? --> probably yes if output is captured
# in the server
outputs.clear()
del ycell["outputs"][:]

elif msg_type == "update_display_data":
Expand All @@ -55,6 +123,14 @@ def output_hook(ycell: pycrdt.Map, msg: dict) -> None:


class NotebookModel(MutableSequence):
"""Notebook model.
Its API is based on a mutable sequence of cells.
"""

# FIXME add notebook metadata
# FIXME add notebook state (TBC)

def __init__(self) -> None:
self._doc = YNotebook()

Expand All @@ -70,39 +146,72 @@ def __getitem__(self, index: int) -> NotebookNode:
nbcell = NotebookNode(**cell)
return nbcell

def __setitem__(self, index: int, value: NotebookNode) -> None:
def __setitem__(self, index: int, value: dict[str, t.Any]) -> None:
self._doc.set_cell(index, value)

def __len__(self) -> int:
"""Number of cells"""
return self._doc.cell_number

def add_code_cell(self, source: str, **kwargs) -> int:
"""Add a code cell
Args:
source: Code cell source
Returns:
Index of the newly added cell
"""
cell = current_api.new_code_cell(source, **kwargs)

self._doc.append_cell(cell)

return len(self) - 1

def add_markdown_cell(self, source: str, **kwargs) -> int:
"""Add a markdown cell
Args:
source: Markdown cell source
Returns:
Index of the newly added cell
"""
cell = current_api.new_markdown_cell(source, **kwargs)

self._doc.append_cell(cell)

return len(self) - 1

def add_raw_cell(self, source: str, **kwargs) -> int:
"""Add a raw cell
Args:
source: Raw cell source
Returns:
Index of the newly added cell
"""
cell = current_api.new_raw_cell(source, **kwargs)

self._doc.append_cell(cell)

return len(self) - 1

def _reset_y_model(self) -> None:
"""Reset the Y model."""
self._doc = YNotebook()
def execute_cell(self, index: int, kernel_client: KernelClient) -> dict:
"""Execute a cell given by its index with the provided kernel client.
The outputs will directly be stored within the notebook model.
Args:
index: Index of the cell to be executed
kernel_client: Kernel client to use
Returns:
Execution results {"execution_count": int | None, "status": str, "outputs": list[dict]}
def execute_cell(self, index: int, kernel_client: t.Any) -> None:
The outputs will follow the structure of nbformat outputs.
"""
ycell = t.cast(pycrdt.Map, self._doc.ycells[index])
source = ycell["source"].to_py()

Expand All @@ -112,8 +221,9 @@ def execute_cell(self, index: int, kernel_client: t.Any) -> None:
ycell["execution_count"] = None
ycell["execution_state"] = "running"

outputs = []
reply = kernel_client.execute_interactive(
source, output_hook=partial(output_hook, ycell), allow_stdin=False
source, output_hook=partial(output_hook, outputs, ycell), allow_stdin=False
)

reply_content = reply["content"]
Expand All @@ -122,6 +232,22 @@ def execute_cell(self, index: int, kernel_client: t.Any) -> None:
ycell["execution_count"] = reply_content.get("execution_count")
ycell["execution_state"] = "idle"

def insert(self, index: int, value: NotebookNode) -> None:
return {
"execution_count": reply_content.get("execution_count"),
"outputs": outputs,
"status": reply_content["status"],
}

def insert(self, index: int, value: dict[str, t.Any]) -> None:
"""Insert a new cell at position index.
Args:
index: The position of the inserted cell
value: A mapping describing the cell
"""
ycell = self._doc.create_ycell(value)
self._doc.ycells.insert(index, ycell)

def _reset_y_model(self) -> None:
"""Reset the Y model."""
self._doc = YNotebook()

0 comments on commit aae60dc

Please sign in to comment.