Files
egutierrez 72c0379c63 feat: enhance jupyter notebook functions with auto-init and kernel management
Auto-create notebooks y sesiones en jupyter_exec (append y cell).
Auto-create en jupyter_write (append_code, append_markdown, batch).
Nuevos subcomandos cleanup y shutdown-all en jupyter_kernel.
README.md renombrado a README.txt para evitar error de parseo del indexer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:10:23 +02:00

378 lines
12 KiB
Python

"""CRUD de kernels Jupyter via REST API."""
import json
import urllib.error
import urllib.request
def _make_request(
method: str,
url: str,
token: str = "",
body: dict | None = None,
) -> dict | list | None:
"""Ejecuta una request HTTP a la API de Jupyter.
Args:
method: Metodo HTTP (GET, POST, DELETE).
url: URL completa del endpoint.
token: Token de autenticacion de Jupyter. Vacio si no se requiere.
body: Cuerpo de la request para metodos POST.
Returns:
Respuesta deserializada como dict o list, o None si la respuesta esta vacia.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
if token:
headers["Authorization"] = f"token {token}"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req) as resp:
raw = resp.read()
if not raw:
return None
return json.loads(raw.decode("utf-8"))
def jupyter_kernel_list(
server_url: str = "http://localhost:8888",
token: str = "",
) -> list[dict]:
"""Lista todos los kernels activos en el servidor Jupyter.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
Returns:
Lista de dicts con los campos: id, name, execution_state,
last_activity, connections.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
url = f"{server_url.rstrip('/')}/api/kernels"
result = _make_request("GET", url, token)
return result if isinstance(result, list) else []
def jupyter_kernel_start(
server_url: str = "http://localhost:8888",
token: str = "",
name: str = "python3",
) -> dict:
"""Inicia un kernel nuevo en el servidor Jupyter.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
name: Nombre del kernel a iniciar (p.ej. "python3", "ir").
Returns:
Dict con los campos: id, name, execution_state.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
url = f"{server_url.rstrip('/')}/api/kernels"
result = _make_request("POST", url, token, body={"name": name})
return result if isinstance(result, dict) else {}
def jupyter_kernel_restart(
server_url: str = "http://localhost:8888",
token: str = "",
kernel_id: str = "",
) -> dict:
"""Reinicia un kernel existente.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
kernel_id: ID del kernel a reiniciar.
Returns:
Dict con la informacion actualizada del kernel.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error (p.ej. 404 si no existe).
urllib.error.URLError: Si no se puede conectar al servidor.
"""
url = f"{server_url.rstrip('/')}/api/kernels/{kernel_id}/restart"
result = _make_request("POST", url, token)
return result if isinstance(result, dict) else {}
def jupyter_kernel_interrupt(
server_url: str = "http://localhost:8888",
token: str = "",
kernel_id: str = "",
) -> None:
"""Interrumpe la ejecucion actual de un kernel.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
kernel_id: ID del kernel a interrumpir.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error (p.ej. 404 si no existe).
urllib.error.URLError: Si no se puede conectar al servidor.
"""
url = f"{server_url.rstrip('/')}/api/kernels/{kernel_id}/interrupt"
_make_request("POST", url, token)
def jupyter_kernel_shutdown(
server_url: str = "http://localhost:8888",
token: str = "",
kernel_id: str = "",
) -> None:
"""Apaga y elimina un kernel.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
kernel_id: ID del kernel a apagar.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error (p.ej. 404 si no existe).
urllib.error.URLError: Si no se puede conectar al servidor.
"""
url = f"{server_url.rstrip('/')}/api/kernels/{kernel_id}"
_make_request("DELETE", url, token)
def jupyter_kernel_sessions(
server_url: str = "http://localhost:8888",
token: str = "",
) -> list[dict]:
"""Lista las sesiones activas del servidor Jupyter.
Cada sesion mapea un notebook a su kernel y usuario actuales.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
Returns:
Lista de dicts con los campos: id, notebook (path), kernel_id,
kernel_state, type, name.
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
url = f"{server_url.rstrip('/')}/api/sessions"
raw = _make_request("GET", url, token)
if not isinstance(raw, list):
return []
sessions = []
for s in raw:
kernel = s.get("kernel") or {}
sessions.append(
{
"id": s.get("id", ""),
"notebook": s.get("path", ""),
"kernel_id": kernel.get("id", ""),
"kernel_state": kernel.get("execution_state", ""),
"type": s.get("type", ""),
"name": s.get("name", ""),
}
)
return sessions
def jupyter_kernel_cleanup(
server_url: str = "http://localhost:8888",
token: str = "",
idle_seconds: int = 3600,
) -> list[dict]:
"""Apaga todos los kernels que llevan mas de idle_seconds sin actividad.
Util para liberar recursos en servidores con muchos notebooks abiertos.
Por defecto cierra kernels inactivos desde hace mas de 1 hora.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
idle_seconds: Segundos de inactividad para considerar un kernel ocioso.
Returns:
Lista de dicts con los kernels apagados (id, name, last_activity, idle_seconds).
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
from datetime import datetime, timezone
kernels = jupyter_kernel_list(server_url, token)
now = datetime.now(timezone.utc)
shutdown_list = []
for k in kernels:
last_activity = k.get("last_activity", "")
if not last_activity:
continue
try:
last_dt = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
idle = (now - last_dt).total_seconds()
except (ValueError, TypeError):
continue
if idle >= idle_seconds:
jupyter_kernel_shutdown(server_url, token, k["id"])
shutdown_list.append({
"id": k["id"],
"name": k.get("name", ""),
"last_activity": last_activity,
"idle_seconds": int(idle),
})
return shutdown_list
def jupyter_kernel_shutdown_all(
server_url: str = "http://localhost:8888",
token: str = "",
) -> list[dict]:
"""Apaga todos los kernels activos del servidor.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
Returns:
Lista de dicts con los kernels apagados (id, name).
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
kernels = jupyter_kernel_list(server_url, token)
shutdown_list = []
for k in kernels:
jupyter_kernel_shutdown(server_url, token, k["id"])
shutdown_list.append({"id": k["id"], "name": k.get("name", "")})
return shutdown_list
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="CRUD de kernels Jupyter via REST API."
)
parser.add_argument(
"--server",
default="http://localhost:8888",
help="URL base del servidor Jupyter (default: http://localhost:8888)",
)
parser.add_argument(
"--token",
default="",
help="Token de autenticacion de Jupyter (default: vacio)",
)
subparsers = parser.add_subparsers(dest="command", required=True)
# list
subparsers.add_parser("list", help="Lista todos los kernels activos.")
# start
sp_start = subparsers.add_parser("start", help="Inicia un kernel nuevo.")
sp_start.add_argument(
"--name",
default="python3",
help="Nombre del kernel (default: python3)",
)
# restart
sp_restart = subparsers.add_parser("restart", help="Reinicia un kernel existente.")
sp_restart.add_argument("kernel_id", help="ID del kernel a reiniciar.")
# interrupt
sp_interrupt = subparsers.add_parser(
"interrupt", help="Interrumpe la ejecucion de un kernel."
)
sp_interrupt.add_argument("kernel_id", help="ID del kernel a interrumpir.")
# shutdown
sp_shutdown = subparsers.add_parser("shutdown", help="Apaga y elimina un kernel.")
sp_shutdown.add_argument("kernel_id", help="ID del kernel a apagar.")
# sessions
subparsers.add_parser("sessions", help="Lista las sesiones activas.")
# cleanup
sp_cleanup = subparsers.add_parser("cleanup", help="Apaga kernels inactivos.")
sp_cleanup.add_argument(
"--idle-seconds",
type=int,
default=3600,
help="Segundos de inactividad para considerar ocioso (default: 3600)",
)
# shutdown-all
subparsers.add_parser("shutdown-all", help="Apaga todos los kernels activos.")
args = parser.parse_args()
try:
if args.command == "list":
result = jupyter_kernel_list(args.server, args.token)
elif args.command == "start":
result = jupyter_kernel_start(args.server, args.token, args.name)
elif args.command == "restart":
result = jupyter_kernel_restart(args.server, args.token, args.kernel_id)
if result is None:
result = {"status": "restarted", "kernel_id": args.kernel_id}
elif args.command == "interrupt":
jupyter_kernel_interrupt(args.server, args.token, args.kernel_id)
result = {"status": "interrupted", "kernel_id": args.kernel_id}
elif args.command == "shutdown":
jupyter_kernel_shutdown(args.server, args.token, args.kernel_id)
result = {"status": "shutdown", "kernel_id": args.kernel_id}
elif args.command == "sessions":
result = jupyter_kernel_sessions(args.server, args.token)
elif args.command == "cleanup":
result = jupyter_kernel_cleanup(args.server, args.token, args.idle_seconds)
elif args.command == "shutdown-all":
result = jupyter_kernel_shutdown_all(args.server, args.token)
else:
parser.print_help()
sys.exit(1)
print(json.dumps(result, indent=2))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(
json.dumps({"error": f"HTTP {e.code}: {e.reason}", "detail": body}),
file=sys.stderr,
)
sys.exit(1)
except urllib.error.URLError as e:
print(
json.dumps({"error": f"URLError: {e.reason}"}),
file=sys.stderr,
)
sys.exit(1)