"""Ejecuta un lote de celdas existentes en una sola conexion WebSocket. A diferencia de `jupyter_execute_cell` (que abre/cierra un WS por celda), esta funcion comparte una unica sesion WS para todas las celdas del lote. Un solo GET /api/contents al inicio + un solo PUT al final. Latencia total: ~3s fija (overhead) + tiempo de ejecucion real de las celdas, en lugar de ~3s * N (una conexion por celda con `jupyter_execute_cell`). """ import json import time from typing import Any from jupyter_kernel_client import KernelClient from notebook.jupyter_exec import ( _ensure_session, _extract_outputs, _get_notebook_content, _kernel_outputs_to_nbformat, _put_notebook_content, ) def jupyter_run_cells( notebook_path: str, cell_indices: list[int], server_url: str = "http://localhost:8888", token: str = "", stop_on_error: bool = True, timeout_per_cell_s: int = 600, ) -> dict[str, Any]: """Ejecuta una lista de celdas existentes (por indice) en un solo paso. Abre UNA conexion WebSocket para todo el lote. Lee el notebook una vez al inicio y lo persiste una vez al final. Si `stop_on_error` es True y una celda produce un output de tipo 'error', el lote para en esa celda y el PUT se hace con los outputs ejecutados hasta ese momento. Args: notebook_path: Ruta relativa al notebook (relativa a la raiz del servidor Jupyter). cell_indices: Lista de indices de celdas a ejecutar (0-based, en orden). Deben ser celdas de tipo 'code'. server_url: URL del servidor Jupyter (default 'http://localhost:8888'). token: Token de autenticacion (default vacio = sin auth). stop_on_error: Si True, para al encontrar el primer error en outputs. timeout_per_cell_s: Timeout en segundos por cada ejecucion individual de celda (pasado a KernelClient.execute). Returns: { "notebook": str, # notebook_path recibido "executed": [ # celdas ejecutadas (en orden) { "cell_index": int, "execution_count": int | None, "outputs": list[str], # strings legibles (via _extract_outputs) "error": str | None, # traceback si hubo error, else None "duration_s": float, }, ... ], "stopped_at": int | None, # indice donde paro si stop_on_error "kernel_id": str, "total_duration_s": float, } """ t_total_start = time.monotonic() kernel_id = _ensure_session(server_url, token, notebook_path) file_node = _get_notebook_content(notebook_path, server_url, token) nb = file_node["content"] cells = nb.get("cells", []) # Validacion anticipada: todos los indices deben estar en rango y ser code for idx in cell_indices: if idx < 0 or idx >= len(cells): raise IndexError( f"cell_index {idx} fuera de rango (notebook tiene {len(cells)} celdas)" ) if cells[idx].get("cell_type") != "code": raise ValueError( f"La celda {idx} no es de codigo (cell_type={cells[idx].get('cell_type')!r})" ) executed: list[dict[str, Any]] = [] stopped_at: int | None = None with KernelClient(server_url=server_url, token=token, kernel_id=kernel_id) as kernel: for idx in cell_indices: cell = cells[idx] source = cell.get("source", "") if isinstance(source, list): source = "".join(source) t_cell_start = time.monotonic() result = kernel.execute(source, timeout=timeout_per_cell_s) duration_s = round(time.monotonic() - t_cell_start, 3) raw_outputs = result.get("outputs", []) cell["outputs"] = _kernel_outputs_to_nbformat(raw_outputs) cell["execution_count"] = result.get("execution_count") readable_outputs = _extract_outputs(raw_outputs) # Detectar si hubo error error_text: str | None = None for out in raw_outputs: if out.get("output_type") == "error": tb = out.get("traceback", []) error_text = "\n".join(tb) if isinstance(tb, list) else str(tb) break executed.append({ "cell_index": idx, "execution_count": result.get("execution_count"), "outputs": readable_outputs, "error": error_text, "duration_s": duration_s, }) if stop_on_error and error_text is not None: stopped_at = idx break # Persiste notebook con todos los outputs actualizados de una vez _put_notebook_content(notebook_path, server_url, token, nb) return { "notebook": notebook_path, "executed": executed, "stopped_at": stopped_at, "kernel_id": kernel_id, "total_duration_s": round(time.monotonic() - t_total_start, 3), } # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- if __name__ == "__main__": import argparse import sys parser = argparse.ArgumentParser( description="Ejecuta un lote de celdas en un solo paso WS" ) parser.add_argument("notebook", help="Ruta al notebook relativa al servidor") parser.add_argument( "indices", nargs="*", type=int, help="Indices de celdas a ejecutar (0-based). Si se omiten, lee JSON de stdin.", ) parser.add_argument("--server", default="http://localhost:8888") parser.add_argument("--token", default="") parser.add_argument( "--no-stop-on-error", action="store_true", help="Continuar aunque una celda emita error", ) parser.add_argument("--timeout", type=int, default=600, help="Timeout por celda en segundos") args = parser.parse_args() if args.indices: indices = args.indices else: raw = sys.stdin.read().strip() indices = json.loads(raw) if raw else [] if not indices: print(json.dumps({"error": "No se especificaron indices de celdas"}), file=sys.stderr) sys.exit(1) try: result = jupyter_run_cells( notebook_path=args.notebook, cell_indices=indices, server_url=args.server, token=args.token, stop_on_error=not args.no_stop_on_error, timeout_per_cell_s=args.timeout, ) print(json.dumps(result, ensure_ascii=False, indent=2)) except Exception as exc: print(json.dumps({"error": str(exc)}, ensure_ascii=False), file=sys.stderr) sys.exit(1)