feat: mejoras notebook functions — discover multi-servidor, write batch ops
jupyter_discover: soporte multi-servidor, detección de modo colaborativo mejorada. jupyter_write: operaciones batch (insert, edit, delete), manejo robusto de Y.js. jupyter_exec: mejoras en ejecución directa al kernel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
"""Operaciones de escritura sobre celdas de un notebook Jupyter via colaboracion en tiempo real.
|
||||
|
||||
NO ejecuta celdas — solo modifica la estructura del notebook (append, insert, edit, delete).
|
||||
NO ejecuta celdas — solo modifica la estructura del notebook (append, insert, edit, delete, create, batch).
|
||||
Usa jupyter_nbmodel_client para comunicarse con el servidor Jupyter via WebSocket.
|
||||
Para crear notebooks usa la API REST de Jupyter (urllib PUT /api/contents).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import argparse
|
||||
from urllib.error import URLError
|
||||
import sys
|
||||
from urllib.error import URLError, HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from jupyter_nbmodel_client import NbModelClient, get_jupyter_notebook_websocket_url
|
||||
@@ -130,6 +132,35 @@ async def _delete_cell(
|
||||
}
|
||||
|
||||
|
||||
async def _batch_write(
|
||||
notebook_path: str,
|
||||
cells: list,
|
||||
server_url: str,
|
||||
token: str,
|
||||
) -> dict:
|
||||
"""Anade multiples celdas en una sola conexion WebSocket."""
|
||||
ws_url = get_jupyter_notebook_websocket_url(
|
||||
server_url=server_url,
|
||||
token=token,
|
||||
path=notebook_path,
|
||||
)
|
||||
username = _resolve_collab_username(server_url, token)
|
||||
async with NbModelClient(ws_url, username=username) as nb:
|
||||
for cell in cells:
|
||||
cell_type = cell.get("type", "code")
|
||||
source = cell.get("source", "")
|
||||
if cell_type == "markdown":
|
||||
nb.add_markdown_cell(source)
|
||||
else:
|
||||
nb.add_code_cell(source)
|
||||
await asyncio.sleep(2)
|
||||
return {
|
||||
"action": "batch",
|
||||
"cells_added": len(cells),
|
||||
"notebook": notebook_path,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API publica sincrona
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -249,6 +280,114 @@ def jupyter_delete_cell(
|
||||
)
|
||||
|
||||
|
||||
def jupyter_create_notebook(
|
||||
notebook_path: str,
|
||||
kernel_name: str = "python3",
|
||||
server_url: str = "http://localhost:8888",
|
||||
token: str = "",
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
"""Crea un notebook vacio (nbformat 4) via la API REST de Jupyter.
|
||||
|
||||
Usa PUT /api/contents/{path} con type: notebook. Si el notebook ya existe
|
||||
y force=False, lanza un error en vez de sobreescribirlo.
|
||||
|
||||
Args:
|
||||
notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
|
||||
kernel_name: Nombre del kernel a asociar (default: python3).
|
||||
server_url: URL base del servidor Jupyter.
|
||||
token: Token de autenticacion del servidor Jupyter.
|
||||
force: Si True, sobreescribe el notebook si ya existe.
|
||||
|
||||
Returns:
|
||||
dict con action, notebook y created.
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
|
||||
# Verificar si ya existe (HEAD request)
|
||||
check_url = f"{server_url}/api/contents/{notebook_path}"
|
||||
check_req = Request(check_url, headers=headers, method="HEAD")
|
||||
already_exists = False
|
||||
try:
|
||||
with urlopen(check_req, timeout=5):
|
||||
already_exists = True
|
||||
except HTTPError as e:
|
||||
if e.code != 404:
|
||||
raise
|
||||
except URLError:
|
||||
raise
|
||||
|
||||
if already_exists and not force:
|
||||
raise FileExistsError(
|
||||
f"Notebook ya existe: {notebook_path}. Usa force=True para sobreescribir."
|
||||
)
|
||||
|
||||
kernel_display = {
|
||||
"python3": "Python 3 (ipykernel)",
|
||||
"python": "Python 3",
|
||||
}.get(kernel_name, kernel_name)
|
||||
|
||||
notebook_content = {
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5,
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": kernel_name,
|
||||
"display_name": kernel_display,
|
||||
"language": "python",
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
},
|
||||
},
|
||||
"cells": [],
|
||||
}
|
||||
|
||||
body = json.dumps({
|
||||
"type": "notebook",
|
||||
"content": notebook_content,
|
||||
}).encode("utf-8")
|
||||
|
||||
put_req = Request(check_url, data=body, headers=headers, method="PUT")
|
||||
with urlopen(put_req, timeout=10) as resp:
|
||||
resp.read()
|
||||
|
||||
return {
|
||||
"action": "create",
|
||||
"notebook": notebook_path,
|
||||
"created": True,
|
||||
}
|
||||
|
||||
|
||||
def jupyter_batch_write(
|
||||
notebook_path: str,
|
||||
cells: list,
|
||||
server_url: str = "http://localhost:8888",
|
||||
token: str = "",
|
||||
) -> dict:
|
||||
"""Anade multiples celdas al notebook en una sola conexion WebSocket.
|
||||
|
||||
Mucho mas eficiente que llamar append-code/append-markdown N veces porque
|
||||
abre una unica conexion WebSocket y hace un solo sleep de sincronizacion
|
||||
al final de todas las inserciones.
|
||||
|
||||
Args:
|
||||
notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
|
||||
cells: Lista de dicts con claves "type" ("code"|"markdown") y "source" (str).
|
||||
server_url: URL base del servidor Jupyter.
|
||||
token: Token de autenticacion del servidor Jupyter.
|
||||
|
||||
Returns:
|
||||
dict con action, cells_added y notebook.
|
||||
"""
|
||||
return asyncio.run(_batch_write(notebook_path, cells, server_url, token))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -299,6 +438,25 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
p_del.add_argument("index", type=int, help="Indice de la celda (0-based)")
|
||||
add_common(p_del)
|
||||
|
||||
# create
|
||||
p_cr = sub.add_parser("create", help="Crea un notebook vacio via API REST")
|
||||
p_cr.add_argument("notebook", help="Ruta del notebook a crear")
|
||||
p_cr.add_argument("--kernel", default="python3", help="Nombre del kernel (default: python3)")
|
||||
p_cr.add_argument("--force", action="store_true", help="Sobreescribir si ya existe")
|
||||
add_common(p_cr)
|
||||
|
||||
# batch
|
||||
p_bt = sub.add_parser("batch", help="Anade multiples celdas en una sola conexion WebSocket")
|
||||
p_bt.add_argument("notebook", help="Ruta del notebook")
|
||||
p_bt.add_argument(
|
||||
"--from",
|
||||
dest="cells_source",
|
||||
default="-",
|
||||
metavar="FILE",
|
||||
help="Archivo JSON con las celdas, o '-' para leer de stdin (default: -)",
|
||||
)
|
||||
add_common(p_bt)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -318,6 +476,22 @@ def main() -> None:
|
||||
result = jupyter_edit_cell(args.notebook, args.index, args.source, args.server, args.token)
|
||||
elif args.command == "delete":
|
||||
result = jupyter_delete_cell(args.notebook, args.index, args.server, args.token)
|
||||
elif args.command == "create":
|
||||
result = jupyter_create_notebook(
|
||||
args.notebook,
|
||||
kernel_name=args.kernel,
|
||||
server_url=args.server,
|
||||
token=args.token,
|
||||
force=args.force,
|
||||
)
|
||||
elif args.command == "batch":
|
||||
if args.cells_source == "-":
|
||||
raw = sys.stdin.read()
|
||||
else:
|
||||
with open(args.cells_source, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
cells = json.loads(raw)
|
||||
result = jupyter_batch_write(args.notebook, cells, args.server, args.token)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user