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,121 @@
|
||||
---
|
||||
name: duckdb_to_postgres
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def duckdb_to_postgres(duckdb_path: str, table: str, pg_dsn: str, pg_table: str = None, mode: str = 'replace', key_cols: list = None, batch_size: int = 5000) -> dict"
|
||||
description: "Pipeline que sincroniza una tabla DuckDB a PostgreSQL. Es lo que desbloquea que herramientas BI (Metabase, Grafana, Superset) lean datos que viven en DuckDB, porque NO hablan DuckDB nativo pero todas hablan PostgreSQL. Pasos: (a) lee el schema con duckdb_table_schema; (b) mapea tipos DuckDB->PostgreSQL (BIGINT/INTEGER->BIGINT, DOUBLE/FLOAT->DOUBLE PRECISION, VARCHAR/TEXT->TEXT, BOOLEAN->BOOLEAN, DATE->DATE, TIMESTAMP->TIMESTAMP, resto->TEXT) y genera CREATE TABLE IF NOT EXISTS con PRIMARY KEY si key_cols (DROP TABLE IF EXISTS antes si mode='replace'), aplicandolo con pg_apply_sql; (c) lee las filas con duckdb_query_readonly paginando con LIMIT/OFFSET e inserta en PostgreSQL con pg_insert_rows (add_snapshot_date=False) en lotes de batch_size, o con pg_upsert si hay key_cols y mode!='replace'. pg_upsert se importa detras de un check de import: sin el, el camino upsert no esta disponible pero replace/append funcionan. Compone funciones del registry sin reescribir su logica. Devuelve un dict sin lanzar: {status:'ok', pg_table, rows_synced, created} en exito y {status:'error', error} en fallo. Depende de duckdb (1.5.2) y psycopg2."
|
||||
tags: [duckdb, postgres, etl, sync, pipeline]
|
||||
uses_functions:
|
||||
- duckdb_table_schema_py_infra
|
||||
- duckdb_query_readonly_py_infra
|
||||
- pg_apply_sql_py_infra
|
||||
- pg_insert_rows_py_infra
|
||||
- pg_upsert_py_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: [os, re, sys, tempfile, duckdb, psycopg2]
|
||||
params:
|
||||
- name: duckdb_path
|
||||
desc: "ruta al archivo DuckDB de origen (se lee en modo read_only; debe existir)."
|
||||
- name: table
|
||||
desc: "nombre de la tabla DuckDB a sincronizar. Validado como identificador ^[A-Za-z_][A-Za-z0-9_]*$."
|
||||
- name: pg_dsn
|
||||
desc: "cadena de conexion PostgreSQL, p.ej. 'postgresql://user:pass@host:5432/db'."
|
||||
- name: pg_table
|
||||
desc: "nombre de la tabla destino en PostgreSQL. None (default) usa el mismo nombre que `table`. Validado como identificador."
|
||||
- name: mode
|
||||
desc: "'replace' (default) hace DROP TABLE IF EXISTS + CREATE + INSERT de todas las filas (snapshot completo). 'append'/'upsert' crean la tabla si no existe y luego: con key_cols usan pg_upsert (idempotente), sin key_cols hacen INSERT append-only. Otro valor devuelve {status:'error'}."
|
||||
- name: key_cols
|
||||
desc: "lista de columnas de la PRIMARY KEY. Se incluyen en el CREATE como PRIMARY KEY y, en modo != 'replace', habilitan el upsert idempotente. None/[] (default) = sin PK, solo INSERT. Deben existir en el schema DuckDB."
|
||||
- name: batch_size
|
||||
desc: "numero de filas por lote de insercion/upsert (default 5000). Debe ser un entero positivo."
|
||||
output: "dict. En exito: {status:'ok', pg_table:str, rows_synced:int, created:bool} donde rows_synced es el total de filas volcadas y created indica si se ejecuto el CREATE/DROP del schema. En error (sin lanzar): {status:'error', error:str}."
|
||||
tested: true
|
||||
tests:
|
||||
- "test_map_tipos_duckdb_a_postgres"
|
||||
- "test_build_ddl_con_pk_y_drop"
|
||||
- "test_build_ddl_sin_pk_ni_drop"
|
||||
- "test_identificador_tabla_invalido"
|
||||
- "test_mode_invalido"
|
||||
- "test_replace_sincroniza_filas"
|
||||
- "test_upsert_idempotente_con_key_cols"
|
||||
test_file_path: "python/functions/pipelines/duckdb_to_postgres_test.py"
|
||||
file_path: "python/functions/pipelines/duckdb_to_postgres.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from pipelines.duckdb_to_postgres import duckdb_to_postgres
|
||||
|
||||
# Snapshot completo: reemplaza la tabla destino en PostgreSQL con todas las filas
|
||||
# de la tabla DuckDB. Metabase/Grafana ya pueden leerla.
|
||||
res = duckdb_to_postgres(
|
||||
"/tmp/almacen.duckdb",
|
||||
"ventas",
|
||||
"postgresql://captacion:****@127.0.0.1:5433/trends",
|
||||
pg_table="ventas_diario",
|
||||
mode="replace",
|
||||
)
|
||||
print(res)
|
||||
# {'status': 'ok', 'pg_table': 'ventas_diario', 'rows_synced': 1280, 'created': True}
|
||||
|
||||
# Sync idempotente por clave: no duplica filas en re-ejecuciones.
|
||||
res2 = duckdb_to_postgres(
|
||||
"/tmp/almacen.duckdb",
|
||||
"clientes",
|
||||
"postgresql://captacion:****@127.0.0.1:5433/trends",
|
||||
mode="upsert",
|
||||
key_cols=["id"],
|
||||
)
|
||||
print(res2) # {'status': 'ok', 'pg_table': 'clientes', 'rows_synced': 540, 'created': True}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tienes datos en un archivo DuckDB y necesitas que una herramienta BI los
|
||||
lea: Metabase, Grafana y Superset NO hablan DuckDB nativo, pero todas hablan
|
||||
PostgreSQL. Es el ultimo eslabon del flujo `Excel -> DuckDB -> PostgreSQL`
|
||||
(precedido por `excel_to_duckdb_py_infra`). Usa `mode='replace'` para refrescos
|
||||
completos programados (un snapshot diario que recrea la tabla) y
|
||||
`mode='upsert' + key_cols` para sincronizaciones incrementales idempotentes que no
|
||||
duplican filas al re-ejecutar.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **DuckDB es single-writer**: el pipeline abre la base en read_only para leer, pero
|
||||
si otro proceso la tiene bloqueada en escritura con version distinta del motor, la
|
||||
apertura puede fallar; el error se devuelve en el dict, no se lanza.
|
||||
- **El modo read_only exige que el archivo DuckDB exista**: no lo crea. Un
|
||||
`duckdb_path` inexistente devuelve `{status:'error', ...}` ya en el paso (a).
|
||||
- **Mapeo de tipos con posible perdida**: el mapeo DuckDB->PostgreSQL es conservador.
|
||||
Tipos no contemplados (DECIMAL con escala, HUGEINT/UBIGINT de 128 bits, LIST/STRUCT/
|
||||
MAP) caen a TEXT. Si el tipado fuerte importa aguas abajo (agregaciones numericas
|
||||
en Metabase), revisa el schema con `duckdb_table_schema_py_infra` y ajusta los tipos
|
||||
en DuckDB antes de sincronizar.
|
||||
- **`mode='replace'` es destructivo**: hace `DROP TABLE IF EXISTS` sobre la tabla
|
||||
PostgreSQL destino antes de recrearla. Cualquier dato o indice manual de esa tabla
|
||||
se pierde. Para sincronizaciones que deban preservar la tabla existente usa
|
||||
`mode='append'`/`'upsert'` (CREATE TABLE IF NOT EXISTS, sin DROP).
|
||||
- **`pg_upsert` opcional**: se importa detras de un check de import. Si `pg_upsert_py_infra`
|
||||
no esta en el entorno, `mode != 'replace'` con `key_cols` devuelve
|
||||
`{status:'error', ...}` explicando que falta; el camino replace/append (sin upsert)
|
||||
sigue funcionando.
|
||||
- **Upsert requiere PRIMARY KEY o UNIQUE** sobre las `key_cols` en PostgreSQL para que
|
||||
`ON CONFLICT` funcione. El pipeline crea esa PRIMARY KEY en el CREATE cuando pasas
|
||||
`key_cols`; si la tabla ya existia sin esa restriccion (`mode!='replace'` y tabla
|
||||
preexistente), el upsert fallara — recrea con `mode='replace' + key_cols` una vez.
|
||||
- **Snapshot no transaccional entre lectura y escritura**: la lectura paginada de
|
||||
DuckDB y la escritura a PostgreSQL no comparten transaccion. Si la tabla DuckDB
|
||||
cambia a mitad del volcado (otro escritor), el resultado en PostgreSQL puede mezclar
|
||||
estados. Sincroniza desde una base DuckDB estable (no mientras se ingesta).
|
||||
- **`pg_insert_rows` y `pg_apply_sql` lanzan** RuntimeError internamente; el pipeline
|
||||
los envuelve en try/except y convierte el fallo a `{status:'error', ...}`. Nunca
|
||||
propaga la excepcion al caller.
|
||||
Reference in New Issue
Block a user