72c0379c63
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>
378 lines
12 KiB
Python
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)
|