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>
114 lines
4.9 KiB
Python
114 lines
4.9 KiB
Python
"""Ingesta una hoja de un archivo .xlsx a una tabla DuckDB.
|
|
|
|
Funcion impura: abre el archivo DuckDB destino en modo read-write
|
|
(`duckdb.connect(duckdb_path)`, que crea el archivo si no existe), carga la
|
|
extension `excel` de DuckDB y materializa la hoja del .xlsx en una tabla con
|
|
`read_xlsx`. La conexion se cierra siempre en un bloque try/finally. Devuelve un
|
|
dict sin lanzar excepciones, siguiendo el estilo del grupo duckdb del registry:
|
|
{status:'ok', ...} en exito y {status:'error', error:str} en fallo.
|
|
|
|
Camino activo (verificado en DuckDB 1.5.2): extension nativa `excel`. El path del
|
|
.xlsx y el nombre de la hoja se pasan como parametros posicionales (marcador `?`)
|
|
a `read_xlsx`, por lo que NO se interpolan en el SQL y no hay inyeccion por esa
|
|
via. El identificador de tabla destino SI se interpola (CREATE/INSERT no admiten
|
|
parametro para el nombre de tabla), asi que se valida contra un regex estricto.
|
|
|
|
mode='replace' (default) -> `CREATE OR REPLACE TABLE <table> AS SELECT * FROM
|
|
read_xlsx(?)`: reemplaza la tabla entera. mode='append' -> crea la tabla si no
|
|
existe (`CREATE TABLE IF NOT EXISTS ... AS SELECT ... LIMIT 0` para fijar el
|
|
schema) y luego `INSERT INTO <table> SELECT * FROM read_xlsx(?)`.
|
|
"""
|
|
|
|
import re
|
|
|
|
# Identificador de tabla valido: letras, digitos y guion bajo, sin empezar por
|
|
# digito. Rechaza cualquier cosa que pudiera inyectarse en el CREATE/INSERT.
|
|
_VALID_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
|
|
|
|
def excel_to_duckdb(
|
|
xlsx_path: str,
|
|
duckdb_path: str,
|
|
table: str,
|
|
sheet: str = None,
|
|
mode: str = "replace",
|
|
) -> dict:
|
|
"""Ingesta una hoja de un .xlsx a una tabla DuckDB via la extension excel.
|
|
|
|
Args:
|
|
xlsx_path: ruta al archivo .xlsx de origen. Debe existir y ser legible.
|
|
Se pasa como parametro posicional a read_xlsx (no se interpola).
|
|
duckdb_path: ruta al archivo DuckDB destino. Se abre en modo escritura, que
|
|
crea el archivo si no existe. DuckDB es single-writer: si otro proceso
|
|
lo tiene abierto en escritura, falla con error de lock.
|
|
table: nombre de la tabla destino. Se valida contra
|
|
^[A-Za-z_][A-Za-z0-9_]*$ antes de interpolarlo en el SQL (CREATE/INSERT
|
|
no admiten parametro para el nombre de tabla). Identificador invalido
|
|
devuelve {status:'error', ...} sin tocar la base.
|
|
sheet: nombre de la hoja a leer. None (default) lee la primera hoja del
|
|
libro. Se pasa como parametro posicional (sheet=?) a read_xlsx.
|
|
mode: 'replace' (default) reemplaza la tabla entera con CREATE OR REPLACE
|
|
TABLE AS SELECT. 'append' crea la tabla si no existe y luego inserta
|
|
las filas con INSERT INTO ... SELECT. Cualquier otro valor devuelve
|
|
{status:'error', ...}.
|
|
|
|
Returns:
|
|
dict. En exito: {status:'ok', table:str, row_count:int} donde row_count es
|
|
el numero de filas que tiene la tabla tras la ingesta. En error (sin
|
|
lanzar): {status:'error', error:str}.
|
|
"""
|
|
if not isinstance(table, str) or not _VALID_IDENT.match(table):
|
|
return {
|
|
"status": "error",
|
|
"error": f"invalid table identifier: {table!r}",
|
|
}
|
|
if mode not in ("replace", "append"):
|
|
return {
|
|
"status": "error",
|
|
"error": f"invalid mode: {mode!r} (expected 'replace' or 'append')",
|
|
}
|
|
|
|
quoted = '"' + table.replace('"', '""') + '"'
|
|
|
|
# Argumentos de read_xlsx: path siempre, sheet solo si se especifica. Todo
|
|
# como parametros posicionales para evitar inyeccion via el .xlsx/hoja.
|
|
if sheet is not None:
|
|
read_call = "read_xlsx(?, sheet=?)"
|
|
read_params = [xlsx_path, sheet]
|
|
else:
|
|
read_call = "read_xlsx(?)"
|
|
read_params = [xlsx_path]
|
|
|
|
conn = None
|
|
try:
|
|
conn = __import__("duckdb").connect(duckdb_path)
|
|
# La extension excel se instala (red la 1a vez) y carga en la conexion.
|
|
conn.execute("INSTALL excel; LOAD excel;")
|
|
|
|
if mode == "replace":
|
|
conn.execute(
|
|
f"CREATE OR REPLACE TABLE {quoted} AS SELECT * FROM {read_call}",
|
|
read_params,
|
|
)
|
|
else: # append
|
|
# Fijamos el schema de la tabla con un SELECT vacio si no existe, sin
|
|
# cargar datos; luego insertamos todas las filas.
|
|
conn.execute(
|
|
f"CREATE TABLE IF NOT EXISTS {quoted} AS "
|
|
f"SELECT * FROM {read_call} LIMIT 0",
|
|
read_params,
|
|
)
|
|
conn.execute(
|
|
f"INSERT INTO {quoted} SELECT * FROM {read_call}",
|
|
read_params,
|
|
)
|
|
|
|
conn.commit()
|
|
row_count = conn.execute(f"SELECT COUNT(*) FROM {quoted}").fetchone()[0]
|
|
return {"status": "ok", "table": table, "row_count": int(row_count)}
|
|
except Exception as e: # noqa: BLE001
|
|
return {"status": "error", "error": str(e)}
|
|
finally:
|
|
if conn is not None:
|
|
conn.close()
|