Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preparation for first release #20

Merged
merged 5 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: v4.3.0
_commit: v4.3.4
_src_path: https://github.com/jupyterlab/extension-template
author_email: ''
author_name: Datalayer
Expand All @@ -12,3 +12,4 @@ project_short_description: A Jupyter Server extension to execute code cell from
python_name: jupyter_server_nbmodel
repository: https://github.com/datalayer/jupyter-server-nbmodel
test: true

40 changes: 38 additions & 2 deletions .github/workflows/update-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ permissions:

jobs:
update-snapshots:
if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') }}
if: >
(
github.event.issue.author_association == 'OWNER' ||
github.event.issue.author_association == 'COLLABORATOR' ||
github.event.issue.author_association == 'MEMBER'
) && github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots')
runs-on: ubuntu-latest

steps:
Expand All @@ -25,10 +30,40 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Get PR Info
id: pr
env:
PR_NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
COMMENT_AT: ${{ github.event.comment.created_at }}
run: |
pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})"
head_sha="$(echo "$pr" | jq -r .head.sha)"
pushed_at="$(echo "$pr" | jq -r .pushed_at)"

if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then
echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)"
exit 1
fi

echo "head_sha=$head_sha" >> $GITHUB_OUTPUT

- name: Checkout the branch from the PR that triggered the job
run: gh pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr checkout ${{ github.event.issue.number }}

- name: Validate the fetched branch HEAD revision
env:
EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
actual_sha="$(git rev-parse HEAD)"

if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then
echo "The HEAD of the checked out branch ($actual_sha) differs from the HEAD commit available at the time when trigger comment was submitted ($EXPECTED_SHA)"
exit 1
fi

- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
Expand All @@ -48,3 +83,4 @@ jobs:
# Playwright knows how to start JupyterLab server
start_server_script: 'null'
test_folder: ui-tests
npm_client: jlpm
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Github Actions Status](https://github.com/datalayer/jupyter-server-nbmodel/workflows/Build/badge.svg)](https://github.com/datalayer/jupyter-server-nbmodel/actions/workflows/build.yml)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/datalayer/jupyter-server-nbmodel/main?urlpath=lab)

A Jupyter Server extension to execute code cell from the server.

This extension is composed of a Python package named `jupyter_server_nbmodel`
Expand All @@ -11,7 +12,8 @@ for the frontend extension.
## Requirements

- Jupyter Server
- \[optional\] JupyterLab >= 4.0.0
- \[recommended\] Real-time collaboration for JupyterLab/Notebook:
This will push the kernels results in the notebook from the server.

## Install

Expand All @@ -21,6 +23,12 @@ To install the extension, execute:
pip install jupyter_server_nbmodel
```

Or with recommendations:

```bash
pip install jupyter_server_nbmodel[rtc]
```

## Uninstall

To remove the extension, execute:
Expand Down
77 changes: 62 additions & 15 deletions jupyter_server_nbmodel/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

if t.TYPE_CHECKING:
import jupyter_client
from nbformat import NotebookNode

try:
import jupyter_server_ydoc
Expand Down Expand Up @@ -74,6 +75,14 @@ async def _get_ycell(
ydoc: jupyter_server_ydoc.app.YDocExtension | None,
metadata: dict | None,
) -> y.Map | None:
"""Get the cell from which the execution was triggered.

Args:
ydoc: The YDoc jupyter server extension
metadata: Execution context
Returns:
The cell
"""
if ydoc is None:
msg = "jupyter-collaboration extension is not installed on the server. Outputs won't be written within the document." # noqa: E501
get_logger().warning(msg)
Expand All @@ -89,7 +98,7 @@ async def _get_ycell(
get_logger().debug(msg)
return None

notebook: YNotebook = await ydoc.get_document(room_id=document_id, copy=False)
notebook: YNotebook | None = await ydoc.get_document(room_id=document_id, copy=False)

if notebook is None:
msg = f"Document with ID {document_id} not found."
Expand Down Expand Up @@ -118,7 +127,14 @@ async def _get_ycell(
return ycell


def _output_hook(ycell, outputs, msg) -> None:
def _output_hook(outputs: list[NotebookNode], ycell: y.Map | None, 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
"""
msg_type = msg["header"]["msg_type"]
if msg_type in ("display_data", "stream", "execute_result", "error"):
# FIXME support for version
Expand Down Expand Up @@ -162,6 +178,13 @@ def _stdin_hook(kernel_id: str, request_id: str, pending_input: PendingInput, ms
"""Callback on stdin message.

It will register the pending input as temporary answer to the execution request.

Args:
kernel_id: The Kernel ID
request_id: The request ID that triggers the input request
pending_input: The pending input description.
This object will be mutated with useful information from ``msg``.
msg: The stdin msg
"""
get_logger().debug(f"Execution request {kernel_id} received a input request.")
if PendingInput.request_id is not None:
Expand All @@ -184,6 +207,17 @@ async def _execute_snippet(
metadata: dict | None,
stdin_hook: t.Callable[[dict], None] | None,
) -> dict[str, t.Any]:
"""Snippet executor

Args:
client: Kernel client
ydoc: Jupyter server YDoc extension
snippet: The code snippet to execute
metadata: The code snippet metadata; e.g. to define the snippet context
stdin_hook: The stdin message callback
Returns:
The execution status and outputs.
"""
ycell = None
if metadata is not None:
ycell = await _get_ycell(ydoc, metadata)
Expand All @@ -201,7 +235,7 @@ async def _execute_snippet(
client.execute_interactive(
snippet,
# FIXME stream partial results
output_hook=partial(_output_hook, ycell, outputs),
output_hook=partial(_output_hook, outputs, ycell),
stdin_hook=stdin_hook if client.allow_stdin else None,
)
)
Expand Down Expand Up @@ -327,21 +361,27 @@ async def cancel(self, kernel_id: str, timeout: float | None = None) -> None:

Args:
kernel_id : Kernel identifier
timeout: Timeout to await for completion in seconds

Raises:
TimeoutError: if a task is not cancelled in time
"""
# FIXME connect this to kernel lifecycle
get_logger().debug(f"Cancel execution for kernel {kernel_id}.")
worker = self.__workers.pop(kernel_id, None)
if worker is not None:
worker.cancel()
await asyncio.wait_for(worker, timeout=timeout)

queue = self.__tasks.pop(kernel_id, None)
if queue is not None:
await asyncio.wait_for(queue.join(), timeout=timeout)

client = self.__kernel_clients.pop(kernel_id, None)
if client is not None:
client.stop_channels()
try:
worker = self.__workers.pop(kernel_id, None)
if worker is not None:
worker.cancel()
await asyncio.wait_for(worker, timeout=timeout)
finally:
try:
queue = self.__tasks.pop(kernel_id, None)
if queue is not None:
await asyncio.wait_for(queue.join(), timeout=timeout)
finally:
client = self.__kernel_clients.pop(kernel_id, None)
if client is not None:
client.stop_channels()

async def send_input(self, kernel_id: str, value: str) -> None:
"""Send input ``value`` to the kernel ``kernel_id``.
Expand Down Expand Up @@ -431,6 +471,13 @@ def put(self, kernel_id: str, snippet: str, metadata: dict | None = None) -> str
return uid

def _get_client(self, kernel_id: str) -> jupyter_client.asynchronous.client.AsyncKernelClient:
"""Get the cached kernel client for ``kernel_id``.

Args:
kernel_id: The kernel ID
Returns:
The client for the given kernel.
"""
if kernel_id not in self.__kernel_clients:
km = self.__manager.get_kernel(kernel_id)
self.__kernel_clients[kernel_id] = km.client()
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ dependencies = [
dynamic = ["version", "description", "authors", "urls", "keywords"]

[project.optional-dependencies]
rtc = ["jupyterlab>=4.2.0", "jupyter_collaboration>=3.0.0a0"]
# FIXME we should be able to only use the server part without UI
rtc = ["jupyterlab>=4.2.0", "jupyter_collaboration>=3.0.0b0"]
test = ["pytest~=8.2", "pytest-cov", "pytest-jupyter[server]>=0.6", "pytest-timeout"]
lint = ["mdformat>0.7", "mdformat-gfm>=0.3.5", "ruff>=0.4.0"]
typing = ["mypy>=0.990"]
Expand Down
2 changes: 1 addition & 1 deletion ui-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ in [jupyter_server_test_config.py](./jupyter_server_test_config.py).

The default configuration will produce video for failing tests and an HTML report.

> There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0).
> There is a UI mode that you may like; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0).

## Run the tests

Expand Down
Loading