feat(infra): grupo claude-fleet — FleetView TUI + orquestacion de Claudes
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI: - list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals + runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId. - list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables. - wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console (preserva ancho del sidebar), tmux_map_claude_panes. - launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r, mouse on, remain-on-exit off; fija el ancho del sidebar con hooks. - docs/capabilities/claude-fleet.md + entrada en el INDEX. Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes varios de docs e infra de otra sesion, agrupados aqui para no perderlos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
"""Anade un grafico nativo de openpyxl a una hoja existente de un libro .xlsx.
|
||||
|
||||
Funcion impura: abre un libro Excel existente, crea un objeto Chart de openpyxl
|
||||
(BarChart/LineChart/PieChart/ScatterChart) sobre rangos de celdas YA escritos, lo
|
||||
ancla en una celda destino y guarda el libro. Es la pieza que faltaba en el grupo
|
||||
`excel` para producir hojas de Excel con graficos: primero se escriben los datos
|
||||
(p.ej. con write_xlsx_sheets) y luego se les anade el chart refiriendo sus rangos.
|
||||
|
||||
No lanza: cualquier fallo (libro inexistente, hoja inexistente, chart_type
|
||||
invalido, openpyxl ausente) se devuelve como dict {"status": "error", ...}.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# chart_type -> clase de openpyxl.chart. Se resuelve perezosamente en runtime
|
||||
# para no fallar al importar el modulo si openpyxl no esta instalado.
|
||||
_VALID_CHART_TYPES = ("bar", "line", "pie", "scatter")
|
||||
|
||||
|
||||
def add_xlsx_chart(
|
||||
xlsx_path: str,
|
||||
sheet_name: str,
|
||||
chart_type: str,
|
||||
data_range: str,
|
||||
cats_range: str = None,
|
||||
anchor: str = "H2",
|
||||
title: str = "",
|
||||
x_title: str = "",
|
||||
y_title: str = "",
|
||||
) -> dict:
|
||||
"""Anade un grafico nativo a una hoja existente de un libro existente.
|
||||
|
||||
Args:
|
||||
xlsx_path: Ruta al archivo .xlsx existente. Debe existir (esta funcion no
|
||||
crea el libro: escribe primero los datos con otra funcion del grupo).
|
||||
sheet_name: Nombre de la hoja (ya existente) donde se ancla el grafico y
|
||||
de la que provienen los rangos.
|
||||
chart_type: Tipo de grafico. Uno de: "bar", "line", "pie", "scatter".
|
||||
data_range: Rango de celdas de los valores a graficar, en notacion Excel
|
||||
tipo "B1:B10". Se convierte a openpyxl.chart.Reference. Si abarca la
|
||||
cabecera (fila 1), se pasa titles_from_data=True para tomar el nombre
|
||||
de la serie de esa primera celda.
|
||||
cats_range: Rango de las categorias/etiquetas del eje X (o labels de pie),
|
||||
tipo "A2:A10". None (default) = sin categorias explicitas. Ignorado
|
||||
para scatter (que usa xvalues, ver Gotchas).
|
||||
anchor: Celda destino (esquina superior izquierda) del grafico, p.ej.
|
||||
"H2". Default "H2".
|
||||
title: Titulo del grafico. Vacio (default) = sin titulo.
|
||||
x_title: Titulo del eje X. Vacio (default) = sin titulo. Ignorado por pie.
|
||||
y_title: Titulo del eje Y. Vacio (default) = sin titulo. Ignorado por pie.
|
||||
|
||||
Returns:
|
||||
Dict. En exito:
|
||||
{"status": "ok", "chart_type": <str>, "sheet": <str>, "anchor": <str>}.
|
||||
En error:
|
||||
{"status": "error", "error": "<mensaje>"}.
|
||||
"""
|
||||
if not xlsx_path:
|
||||
return {"status": "error", "error": "xlsx_path no puede estar vacio"}
|
||||
|
||||
ct = (chart_type or "").strip().lower()
|
||||
if ct not in _VALID_CHART_TYPES:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": (
|
||||
f"chart_type invalido: '{chart_type}'. "
|
||||
f"Validos: {list(_VALID_CHART_TYPES)}"
|
||||
),
|
||||
}
|
||||
|
||||
abs_path = os.path.abspath(xlsx_path)
|
||||
if not os.path.exists(abs_path):
|
||||
return {"status": "error", "error": f"libro no encontrado: {abs_path}"}
|
||||
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.chart import (
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
Reference,
|
||||
ScatterChart,
|
||||
Series,
|
||||
)
|
||||
from openpyxl.utils.cell import range_boundaries
|
||||
except ImportError: # pragma: no cover - dependencia del entorno
|
||||
return {
|
||||
"status": "error",
|
||||
"error": (
|
||||
"openpyxl es requerido para add_xlsx_chart. "
|
||||
"Instalar con: cd python && uv add openpyxl"
|
||||
),
|
||||
}
|
||||
|
||||
try:
|
||||
wb = load_workbook(abs_path)
|
||||
except Exception as exc: # noqa: BLE001 - contrato del grupo: no lanzar
|
||||
return {"status": "error", "error": f"no se pudo abrir el libro: {exc}"}
|
||||
|
||||
if sheet_name not in wb.sheetnames:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": (
|
||||
f"hoja '{sheet_name}' no existe. "
|
||||
f"Hojas disponibles: {wb.sheetnames}"
|
||||
),
|
||||
}
|
||||
|
||||
ws = wb[sheet_name]
|
||||
|
||||
# Convierte "B1:B10" -> (min_col, min_row, max_col, max_row) en 1-index.
|
||||
try:
|
||||
d_min_col, d_min_row, d_max_col, d_max_row = range_boundaries(data_range)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"data_range invalido '{data_range}': {exc}",
|
||||
}
|
||||
|
||||
chart_classes = {
|
||||
"bar": BarChart,
|
||||
"line": LineChart,
|
||||
"pie": PieChart,
|
||||
"scatter": ScatterChart,
|
||||
}
|
||||
|
||||
try:
|
||||
chart = chart_classes[ct]()
|
||||
if title:
|
||||
chart.title = title
|
||||
|
||||
if ct == "scatter":
|
||||
# Scatter empareja X (cats_range) con Y (data_range) como una serie.
|
||||
chart.style = 13
|
||||
if x_title:
|
||||
chart.x_axis.title = x_title
|
||||
if y_title:
|
||||
chart.y_axis.title = y_title
|
||||
yvalues = Reference(
|
||||
ws,
|
||||
min_col=d_min_col,
|
||||
min_row=d_min_row,
|
||||
max_col=d_max_col,
|
||||
max_row=d_max_row,
|
||||
)
|
||||
if cats_range:
|
||||
try:
|
||||
x_min_col, x_min_row, x_max_col, x_max_row = range_boundaries(
|
||||
cats_range
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"cats_range invalido '{cats_range}': {exc}",
|
||||
}
|
||||
xvalues = Reference(
|
||||
ws,
|
||||
min_col=x_min_col,
|
||||
min_row=x_min_row,
|
||||
max_col=x_max_col,
|
||||
max_row=x_max_row,
|
||||
)
|
||||
series = Series(yvalues, xvalues, title_from_data=False)
|
||||
else:
|
||||
series = Series(yvalues, title_from_data=False)
|
||||
chart.series.append(series)
|
||||
else:
|
||||
# bar/line/pie: add_data + set_categories.
|
||||
if ct in ("bar", "line"):
|
||||
if x_title:
|
||||
chart.x_axis.title = x_title
|
||||
if y_title:
|
||||
chart.y_axis.title = y_title
|
||||
# titles_from_data toma el nombre de serie de la primera fila del
|
||||
# rango cuando este incluye la cabecera (fila 1).
|
||||
from_data = d_min_row == 1
|
||||
data = Reference(
|
||||
ws,
|
||||
min_col=d_min_col,
|
||||
min_row=d_min_row,
|
||||
max_col=d_max_col,
|
||||
max_row=d_max_row,
|
||||
)
|
||||
chart.add_data(data, titles_from_data=from_data)
|
||||
if cats_range:
|
||||
try:
|
||||
c_min_col, c_min_row, c_max_col, c_max_row = range_boundaries(
|
||||
cats_range
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"cats_range invalido '{cats_range}': {exc}",
|
||||
}
|
||||
cats = Reference(
|
||||
ws,
|
||||
min_col=c_min_col,
|
||||
min_row=c_min_row,
|
||||
max_col=c_max_col,
|
||||
max_row=c_max_row,
|
||||
)
|
||||
chart.set_categories(cats)
|
||||
|
||||
ws.add_chart(chart, anchor)
|
||||
wb.save(abs_path)
|
||||
except Exception as exc: # noqa: BLE001 - contrato del grupo: no lanzar
|
||||
return {"status": "error", "error": f"no se pudo anadir el grafico: {exc}"}
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"chart_type": ct,
|
||||
"sheet": sheet_name,
|
||||
"anchor": anchor,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - smoke manual
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from infra.write_xlsx_sheets import write_xlsx_sheets
|
||||
|
||||
tmp = os.path.join(tempfile.gettempdir(), "add_xlsx_chart_demo.xlsx")
|
||||
write_xlsx_sheets(
|
||||
tmp,
|
||||
{
|
||||
"Ventas": [
|
||||
["Mes", "Unidades"],
|
||||
["Ene", 120],
|
||||
["Feb", 150],
|
||||
["Mar", 90],
|
||||
["Abr", 200],
|
||||
],
|
||||
},
|
||||
)
|
||||
res = add_xlsx_chart(
|
||||
xlsx_path=tmp,
|
||||
sheet_name="Ventas",
|
||||
chart_type="bar",
|
||||
data_range="B1:B5", # incluye cabecera "Unidades" -> nombre de serie
|
||||
cats_range="A2:A5", # meses
|
||||
anchor="D2",
|
||||
title="Unidades por mes",
|
||||
x_title="Mes",
|
||||
y_title="Unidades",
|
||||
)
|
||||
print(res)
|
||||
Reference in New Issue
Block a user