927437a8d8
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>
145 lines
4.3 KiB
Python
145 lines
4.3 KiB
Python
"""Tests para read_xlsx.
|
|
|
|
Se importa el modulo por path directo (sin tocar __init__.py) para no depender
|
|
del re-export del paquete. write_xlsx_sheets se importa igual para el round-trip.
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _load(name):
|
|
spec = importlib.util.spec_from_file_location(name, os.path.join(_HERE, f"{name}.py"))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
read_xlsx = _load("read_xlsx").read_xlsx
|
|
write_xlsx_sheets = _load("write_xlsx_sheets").write_xlsx_sheets
|
|
|
|
|
|
def test_round_trip_escribe_lee_compara(tmp_path):
|
|
"""Escribir con write_xlsx_sheets y leer con read_xlsx devuelve los mismos datos."""
|
|
out = str(tmp_path / "rt.xlsx")
|
|
write_xlsx_sheets(
|
|
out,
|
|
{
|
|
"Ventas": [
|
|
["Producto", "Unidades", "Precio", "Activo"],
|
|
["Teclado", 12, 29.99, True],
|
|
["Raton", 30, 14.5, False],
|
|
["Monitor", None, 199.0, True],
|
|
],
|
|
},
|
|
)
|
|
|
|
res = read_xlsx(out)
|
|
assert res["status"] == "ok"
|
|
assert list(res["sheets"].keys()) == ["Ventas"]
|
|
|
|
ventas = res["sheets"]["Ventas"]
|
|
assert ventas["headers"] == ["Producto", "Unidades", "Precio", "Activo"]
|
|
assert ventas["rows"] == [
|
|
["Teclado", 12, 29.99, True],
|
|
["Raton", 30, 14.5, False],
|
|
["Monitor", None, 199.0, True],
|
|
]
|
|
|
|
|
|
def test_lee_solo_la_hoja_indicada(tmp_path):
|
|
out = str(tmp_path / "multi.xlsx")
|
|
write_xlsx_sheets(
|
|
out,
|
|
{
|
|
"A": [["x"], [1]],
|
|
"B": [["y"], [2]],
|
|
},
|
|
)
|
|
|
|
res = read_xlsx(out, sheet="B")
|
|
assert res["status"] == "ok"
|
|
assert list(res["sheets"].keys()) == ["B"]
|
|
assert res["sheets"]["B"]["headers"] == ["y"]
|
|
assert res["sheets"]["B"]["rows"] == [[2]]
|
|
|
|
|
|
def test_max_rows_trunca_filas_de_datos(tmp_path):
|
|
out = str(tmp_path / "trunc.xlsx")
|
|
write_xlsx_sheets(
|
|
out,
|
|
{"S": [["n"], [1], [2], [3], [4], [5]]},
|
|
)
|
|
|
|
res = read_xlsx(out, sheet="S", max_rows=2)
|
|
assert res["status"] == "ok"
|
|
assert res["sheets"]["S"]["headers"] == ["n"]
|
|
assert res["sheets"]["S"]["rows"] == [[1], [2]]
|
|
|
|
|
|
def test_header_false_no_consume_cabecera(tmp_path):
|
|
out = str(tmp_path / "nohdr.xlsx")
|
|
write_xlsx_sheets(out, {"S": [["a", "b"], [1, 2]]})
|
|
|
|
res = read_xlsx(out, sheet="S", header=False)
|
|
assert res["status"] == "ok"
|
|
assert res["sheets"]["S"]["headers"] == []
|
|
assert res["sheets"]["S"]["rows"] == [["a", "b"], [1, 2]]
|
|
|
|
|
|
def test_fecha_se_devuelve_como_iso(tmp_path):
|
|
import datetime
|
|
|
|
from openpyxl import Workbook
|
|
|
|
out = str(tmp_path / "fechas.xlsx")
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.title = "F"
|
|
ws.append(["evento", "cuando"])
|
|
ws.append(["solo_fecha", datetime.date(2026, 6, 16)])
|
|
ws.append(["con_hora", datetime.datetime(2026, 6, 16, 14, 30, 0)])
|
|
wb.save(out)
|
|
|
|
res = read_xlsx(out, sheet="F")
|
|
assert res["status"] == "ok"
|
|
rows = res["sheets"]["F"]["rows"]
|
|
assert rows[0] == ["solo_fecha", "2026-06-16"]
|
|
assert rows[1] == ["con_hora", "2026-06-16T14:30:00"]
|
|
|
|
|
|
def test_formula_se_lee_como_valor_calculado(tmp_path):
|
|
"""data_only lee el valor cacheado de la formula si Excel/openpyxl lo guardo.
|
|
|
|
openpyxl no calcula formulas; cuando escribimos la formula con openpyxl el
|
|
valor cacheado es None hasta que un motor (Excel/LibreOffice) la evalua y
|
|
guarda. El round-trip valido es escribir el VALOR (no la formula).
|
|
"""
|
|
out = str(tmp_path / "calc.xlsx")
|
|
# Escribimos el valor resultante directamente: read_xlsx con data_only lo lee.
|
|
write_xlsx_sheets(out, {"C": [["total"], [42]]})
|
|
res = read_xlsx(out, sheet="C")
|
|
assert res["status"] == "ok"
|
|
assert res["sheets"]["C"]["rows"] == [[42]]
|
|
|
|
|
|
def test_archivo_inexistente_devuelve_error():
|
|
res = read_xlsx("/tmp/no_existe_seguro_123456.xlsx")
|
|
assert res["status"] == "error"
|
|
assert "no encontrado" in res["error"]
|
|
|
|
|
|
def test_hoja_inexistente_devuelve_error(tmp_path):
|
|
out = str(tmp_path / "h.xlsx")
|
|
write_xlsx_sheets(out, {"Real": [["x"], [1]]})
|
|
res = read_xlsx(out, sheet="Fantasma")
|
|
assert res["status"] == "error"
|
|
assert "no existe" in res["error"]
|
|
|
|
|
|
def test_path_vacio_devuelve_error():
|
|
res = read_xlsx("")
|
|
assert res["status"] == "error"
|