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:
2026-06-17 00:04:41 +02:00
parent 7d395f39e5
commit 927437a8d8
58 changed files with 5961 additions and 2 deletions
+4 -1
View File
@@ -24,6 +24,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [docker](docker.md) | 38 | Operar Docker desde Go/Bash: build/run/stop, compose, networks, volumes, logs, deploys |
| [android](android.md) | 37 | Toolbelt Android desde WSL2: adb, emuladores AVD, APK build/install, Capacitor, logcat |
| [web-proxy](web-proxy.md) | 5 | Captura de trafico HTTP/HTTPS liviana (mitmproxy): proxy con rotacion, navegador proxeado, consulta de capturas, tee del SSE de claude. Alternativa ligera a ZAP/Burp |
| [claude-fleet](claude-fleet.md) | 5 | Orquestar la flota de procesos Claude Code vivos: panel TUI (fleetview) + comando kittyclaude que centraliza N Claudes en una ventana kitty/tmux (socket -L fleet), conmuta cual esta embebido (alt+flechas/enter/n) y los lista desde ~/.claude/sessions+goals |
| [flow-replay](flow-replay.md) | 3 | Guardar un flujo web (login, reiniciar server, formulario) como funcion reproducible: destila un HAR a call specs y lo reproduce sin navegador (HTTP puro), con fallback a chromium headless/visible. Consume las capturas de web-proxy |
| [hoppscotch](hoppscotch.md) | 7 | Operar Hoppscotch SELF-HOSTED (docker en selfhost/) via API GraphQL: login (magic link headless via mailpit), CRUD de requests (create/update/delete/list), set_environment (idempotente, resuelve secretos pass:). El agente crea/edita y el humano lo ve en vivo en su GUI (subscriptions). build es helper interno de serializacion. Modo .json local ELIMINADO |
| [dav](dav.md) | 9 | Cliente CardDAV/CalDAV (Python, solo stdlib) para Xandikos: parte un .vcf/.ics export de Google en recursos individuales (split puro), extrae/sintetiza UID, sube por HTTP PUT con Basic auth, lista (PROPFIND) y descarga (GET) recursos. Dos pipelines de import (vcf->carddav, ics->caldav). Formaliza la migracion ad-hoc de contactos/calendario |
@@ -52,7 +53,9 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [terminal-capture](terminal-capture.md) | 6 | Automatizar y capturar el texto de una CLI/TUI interactiva via PTY headless: spawn+input scripteado (one-shot y streaming), render del layout 2D (emulador VT), strip ANSI, delta por prefijo, y parseo de la TUI de claude a datos |
| [claude-direct](claude-direct.md) | 3 | Hablar directamente con la API de Anthropic Messages usando el token OAuth de Claude Code (Claude Max): leer token, stream SSE, bucle agentico de tool-use |
| [obsidian](obsidian.md) | 16 | CRUD headless de vaults y notas Obsidian como Markdown plano (frontmatter YAML + wikilinks): parse/format, read/create/update/delete/list/search notas, list/create vaults, slugify/embeds/resolve, render tabla Markdown + bloques sentinel gestionados. Sin app GUI |
| [duckdb](duckdb.md) | 5 | Operar bases DuckDB: open (Go), query read-only segura (Python, tipos JSON-safe), CSV->Parquet, dedup por hash, carga OHLCV. Base del patron BD-fuente-de-verdad + Obsidian-vista (app osint_db) |
| [duckdb](duckdb.md) | 10 | Operar bases DuckDB: open (Go), query/execute/upsert, introspeccion (list_tables, table_schema), CSV->Parquet, dedup, OHLCV, e ingesta desde Excel (excel_to_duckdb) + salida a Postgres (duckdb_to_postgres). Motor analitico del stack de datos Excel->DuckDB->Postgres->viz |
| [excel](excel.md) | 6 | CRUD de hojas Excel (.xlsx) con openpyxl: escribir multi-hoja, upsert no destructivo (preserva columnas manuales), leer a memoria, leer a markdown, graficos nativos (bar/line/pie/scatter), e ingesta a DuckDB. Round-trip de datos con humanos |
| [postgres](postgres.md) | 7 | CRUD de PostgreSQL via psycopg2 (dsn): connect (Go), query read-only, insert append-only, upsert idempotente, crear tabla inferida, introspeccion, aplicar .sql. Capa que sirve datos a Metabase/Grafana (que no hablan DuckDB nativo) |
| [recon](recon.md) | 8 | Reconocimiento de red OSINT: whois, rdap, dns (dig), ping, traceroute, nmap por perfiles. Cada scan se archiva en OSINT (nota vault + tabla DuckDB network_scans) via el sink save_scan_to_osint o el pipeline one-shot recon_osint. Perfiles nmap pesados (full-tcp/vuln/udp-top) en segundo plano. No es framework de explotacion; solo hosts autorizados |
| [osint-passive](osint-passive.md) | 8 | Recoleccion OSINT pasiva (fuentes publicas, no intrusiva): EXIF/PDF metadata, whois RDAP, DNS, subdominios crt.sh, guess emails, username enumeration, search dorks |
| [osint-enrich](osint-enrich.md) | 3 | Orquestadores de enriquecimiento OSINT: componen osint-passive para aumentar datapoints de personas (emails/usernames/dorks), orgs (whois+dns+subdominios) y metadatos de attachments |
+68
View File
@@ -0,0 +1,68 @@
# Capability group: claude-fleet
Operar la **flota de procesos Claude Code** vivos en la máquina como una sola
unidad: descubrirlos, listarlos en un panel TUI y centralizarlos en una ventana
kitty con tmux donde se conmuta cuál está embebido a la derecha. Reemplaza el
caos de N ventanas kitty dispersas por un único punto de entrada.
Pieza visible: la app `fleetview` (TUI). Entrypoint: el comando `kittyclaude`.
## Funciones
| ID | Firma | Qué hace |
|---|---|---|
| `list_claude_fleet_go_infra` | `ListClaudeFleet() ([]ClaudeFleet, error)` | Escanea `~/.claude/sessions/*.json` + `goals/`, valida procesos vivos (anti-PID-reciclado), join por `sessionId` → lista tipada con status/objetivo/cwd/target. |
| `launch_kittyclaude_bash_infra` | `launch_kittyclaude [--cwd <d>] [--bin <p>] [--session <n>] [--cols <n>]` | Entrypoint: abre kitty con sesión tmux (socket aislado `-L fleet`) de dos panes (TUI izq + Claude der). Instala atajos `alt+*` e hijos del sidebar. |
| `tmux_new_claude_window_go_infra` | `TmuxNewClaudeWindow(socket, session, cwd string) (string, error)` | Crea una window tmux nueva con `claude --dangerously-skip-permissions`. Devuelve el `window_id`. |
| `tmux_swap_window_into_console_go_infra` | `TmuxSwapWindowIntoConsole(socket, session, windowID string) error` | Trae el Claude de `windowID` al pane derecho de `console` (junto a la TUI), parkea el anterior, re-fija el ancho del sidebar. |
| `tmux_map_claude_panes_go_infra` | `TmuxMapClaudePanes(socket string) (map[int]string, error)` | Mapa `claudePID → window_id` de los Claude que viven en la sesión (vía `list-panes` + descendencia `/proc`). Permite a la TUI saber cuáles son conmutables. |
App relacionada: `fleetview_go_infra` (`apps/fleetview/`) — la TUI Bubble Tea que consume `list_claude_fleet` y orquesta los wrappers tmux.
## Ejemplo canónico (end-to-end)
```bash
# 1. Compilar la TUI una vez.
cd ~/fn_registry/apps/fleetview && go build -o fleetview .
# 2. Abrir la flota (una ventana kitty: panel izq + Claude der).
fn run launch_kittyclaude
# 3. Dentro de la ventana, desde CUALQUIER pane (incluido escribiendo en Claude):
# alt+↑/↓ mueve el cursor de la lista
# alt+enter conmuta el pane derecho al Claude seleccionado
# alt+n abre un Claude nuevo (window en fleet) y conmuta a él
# Inspección headless de la flota sin abrir nada:
fn run list_claude_fleet | jq '.[] | {rename, status, goal}'
```
Bajo el capó de `alt+enter`/`alt+n`: tmux redirige la tecla al pane de la TUI
(`bind -n M-Enter send-keys -t console.0 Enter`); la TUI resuelve el Claude
seleccionado con `TmuxMapClaudePanes` y lo trae con `TmuxSwapWindowIntoConsole`
(o crea uno con `TmuxNewClaudeWindow`).
## Fronteras (qué NO cubre)
- **No gestiona Claudes remotos** (ej. los de una sesión tmux del móvil): se
listan como contexto pero no se embeben localmente (no son panes de fleet).
- **Adopción de Claudes sueltos pendiente**: un Claude vivo en otra ventana kitty
(fuera de fleet) se lista, pero `alt+enter` sobre él aún no lo trae —
requerirá relaunch `claude --resume <sessionId>` dentro de fleet (patrón de
`reboot_all_claudes_bash_infra`).
- **No reinicia ni mata Claudes** (todavía): `resume`/`kill` desde el panel son
fase posterior. Para reiniciar toda la flota existe `reboot_all_claudes_bash_infra`.
- **Linux + kitty + tmux** únicamente (build tag `!windows`, usa `/proc`).
## Prerequisitos
- `kitty` y `tmux` en el PATH. La sesión vive en un server tmux aislado (`-L fleet`).
- La TUI `fleetview` compilada (`apps/fleetview/fleetview`).
- Claude Code ≥ 2.1.x (escribe `~/.claude/sessions/<PID>.json` con `status`).
## Notas
- Toda la sesión usa el socket `-L fleet`: los atajos `bind -n` no afectan al
tmux por defecto del usuario; `tmux -L fleet kill-server` lo limpia entero.
- `reboot_all_claudes_bash_infra` comparte la misma fuente de verdad
(`~/.claude/sessions/<PID>.json`) y es el complemento para reiniciar la flota.
+33
View File
@@ -15,6 +15,39 @@ Pieza central del patron **BD como fuente de verdad + Obsidian como vista** (pro
| `csv_to_parquet_duckdb_py_core` | `csv_to_parquet_duckdb(csv_path, parquet_path, column_casts=None, overwrite=False) -> bool` | Convierte CSV -> Parquet con `read_csv_auto`. `column_casts` fuerza tipos por columna. No reescribe si el parquet existe y `overwrite=False`. |
| `dedup_duckdb_table_by_hash_py_pipelines` | `dedup_duckdb_table_by_hash(duckdb_path, table, exclude_cols=None) -> dict` | Pipeline: anade columna `row_hash` (md5 de columnas de datos) idempotentemente y borra filas duplicadas conservando la primera insercion. |
| `load_ohlcv_from_duckdb_go_finance` | `LoadOHLCVFromDuckDB(dbPath, query string) ([][]float64, error)` | Carga datos OHLCV ejecutando una query SQL sobre una base DuckDB (consumo desde apps Go de finanzas). |
| `duckdb_list_tables_py_infra` | `duckdb_list_tables(db_path) -> dict` | Introspección read-only: lista las tablas (`information_schema.tables`, schema main) ordenadas. Devuelve `{status, tables}`. |
| `duckdb_table_schema_py_infra` | `duckdb_table_schema(db_path, table) -> dict` | Introspección read-only: schema de una tabla (`DESCRIBE`). Devuelve `{status, table, columns:[{name,type}]}`. Útil para mapear tipos a otro motor (p.ej. PostgreSQL). |
| `excel_to_duckdb_py_infra` | `excel_to_duckdb(xlsx_path, duckdb_path, table, sheet=None, mode='replace') -> dict` | **Puente de entrada Excel→DuckDB**: ingiere una hoja `.xlsx` a una tabla con la extensión nativa `excel` de DuckDB. `replace`/`append`. Devuelve `{status, table, row_count}`. |
| `duckdb_to_postgres_py_pipelines` | `duckdb_to_postgres(duckdb_path, table, pg_dsn, pg_table=None, mode='replace', key_cols=None, batch_size=5000) -> dict` | **Puente de salida DuckDB→Postgres**: mapea tipos, crea la tabla y sincroniza filas. Desbloquea que Metabase/Grafana/Superset (que no hablan DuckDB) lean los datos. Devuelve `{status, pg_table, rows_synced, created}`. |
## Puentes: Excel → DuckDB → Postgres → visualización
DuckDB es el centro del stack de datos: el motor analítico embebido. Los datos entran desde Excel y salen hacia BI:
```bash
cd /home/enmanuel/fn_registry
python/.venv/bin/python3 - <<'PYEOF'
import sys
sys.path.insert(0, "python/functions")
from infra import excel_to_duckdb, duckdb_list_tables, duckdb_query_readonly
from pipelines.duckdb_to_postgres import duckdb_to_postgres
# 1. Excel -> DuckDB (extensión nativa, sin pandas)
excel_to_duckdb("/tmp/ventas.xlsx", "/tmp/datos.duckdb", "ventas", sheet="ventas")
print(duckdb_list_tables("/tmp/datos.duckdb"))
# 2. Analítica en DuckDB
print(duckdb_query_readonly("/tmp/datos.duckdb",
"SELECT categoria, SUM(importe) AS total FROM ventas GROUP BY 1")["rows"])
# 3. DuckDB -> Postgres (para que Metabase/Grafana lo lean)
# dsn = "postgresql://captacion:<pass>@localhost:5433/trends"
# duckdb_to_postgres("/tmp/datos.duckdb", "ventas", dsn, pg_table="ventas", mode="replace")
PYEOF
```
- **Evidence.dev** lee el `.duckdb` directamente (nativo) — no necesita el puente a Postgres.
- **Metabase / Grafana / Superset** no hablan DuckDB → usa `duckdb_to_postgres` y apunta la herramienta al Postgres espejo.
## Ejemplo canonico
+64
View File
@@ -0,0 +1,64 @@
# Capability: excel
CRUD de hojas de cálculo Excel (`.xlsx`) desde el registry con openpyxl: escribir libros multi-hoja, actualizar una hoja sin destruir las demás (preservando columnas editadas a mano), leer a estructuras en memoria o a markdown, añadir gráficos nativos, e ingerir una hoja a DuckDB.
Es el extremo Excel del **stack de datos** `Excel → DuckDB → Postgres → visualización`: el Excel sirve como entrada (lo que produce un humano o un export) y como entregable (un libro con gráficos que viaja por email/disco, sin servidor). El round-trip humano lo cubre `upsert_xlsx_sheet`, que conserva las columnas que las personas rellenan a mano mientras regenera las columnas calculadas.
## Funciones
| ID | Firma | Que hace |
|---|---|---|
| `write_xlsx_sheets_py_infra` | `write_xlsx_sheets(out_path, sheets, header_bold=True, autofit=True, freeze_header=True) -> str` | Escribe (o sobrescribe) un libro `.xlsx` multi-hoja desde un dict `{nombre_hoja: datos}`. Cada hoja acepta `list[list]` (primera fila = headers) o `{"headers": [...], "rows": [[...]]}`. Cabecera en negrita, auto-ancho, freeze de cabecera. Devuelve la ruta absoluta. |
| `upsert_xlsx_sheet_py_infra` | `upsert_xlsx_sheet(xlsx_path, sheet_name, records, columns, key_col="", preserve_cols=None, formulas=None, backup=True, ...) -> dict` | Actualiza NO destructivamente UNA hoja: reescribe solo `sheet_name` y conserva las demás. Antes de limpiar, lee por `key_col` las columnas de trabajo manual (`preserve_cols`) y las reescribe ganando sobre los datos nuevos. Cabecera estilizada, freeze, autofilter, fórmulas por columna, backup `.bak`. |
| `read_xlsx_py_infra` | `read_xlsx(path, sheet=None, max_rows=None, header=True) -> dict` | Lee un `.xlsx` a memoria (NO a markdown). Devuelve `{status, sheets: {nombre: {headers, rows}}}`. `sheet=None` lee todas. Tipos de celda: fechas→ISO, int/float, bool, None, fórmulas (valor calculado, `data_only=True`). Espejo en lectura de `write_xlsx_sheets`. |
| `excel_to_markdown_py_core` | `excel_to_markdown(path, max_rows_per_sheet=1000) -> str` | Convierte `.xlsx/.xls/.xlsm` a markdown, cada hoja como sección H2. Para inspección rápida / pegar en un prompt o nota. |
| `add_xlsx_chart_py_infra` | `add_xlsx_chart(xlsx_path, sheet_name, chart_type, data_range, cats_range=None, anchor='H2', title='', x_title='', y_title='') -> dict` | Añade un gráfico nativo (`bar`/`line`/`pie`/`scatter`) a una hoja EXISTENTE, refiriendo rangos de celdas ya escritos (notación Excel `'C1:C7'`). `anchor` = celda destino. La pieza para generar hojas Excel CON gráficos. |
| `excel_to_duckdb_py_infra` | `excel_to_duckdb(xlsx_path, duckdb_path, table, sheet=None, mode='replace') -> dict` | Ingesta una hoja del `.xlsx` a una tabla DuckDB con la extensión nativa `excel` de DuckDB. Puente Excel→DuckDB. También etiquetada en el grupo `duckdb`. |
## Ejemplo canónico
Escribir un libro, añadirle un gráfico y releerlo a memoria (verificado):
```bash
cd /home/enmanuel/fn_registry
python/.venv/bin/python3 - <<'PYEOF'
import sys
sys.path.insert(0, "python/functions")
from infra import write_xlsx_sheets, add_xlsx_chart, read_xlsx
xlsx = "/tmp/ventas.xlsx"
write_xlsx_sheets(xlsx, {"ventas": [
["mes", "categoria", "importe"],
["2026-01", "neumaticos", 12500.50],
["2026-02", "neumaticos", 15800.75],
["2026-03", "neumaticos", 18200.00],
]})
# Gráfico de barras del importe por mes, anclado en la celda G2
add_xlsx_chart(xlsx, "ventas", "bar", data_range="C1:C4", cats_range="A2:A4",
anchor="G2", title="Importe por mes", y_title="EUR")
rd = read_xlsx(xlsx, sheet="ventas")
print(rd["sheets"]["ventas"]["headers"], len(rd["sheets"]["ventas"]["rows"]))
PYEOF
```
## Gotchas del grupo
- **openpyxl no evalúa fórmulas.** `read_xlsx` con `data_only=True` devuelve el valor **cacheado** por la última app que guardó el libro (Excel/LibreOffice). Un `.xlsx` con fórmulas escritas por openpyxl y nunca abierto en una hoja de cálculo devuelve `None` en esas celdas.
- **`add_xlsx_chart` exige libro y hoja existentes:** no crea el `.xlsx` ni escribe datos; los rangos deben apuntar a celdas ya escritas. Flujo: `write_xlsx_sheets``add_xlsx_chart`.
- **Rangos 1-indexed, notación Excel** (`'C1:C7'`). Si `data_range` incluye la fila de cabecera, el nombre de la serie sale de esa celda (`titles_from_data`). `scatter` usa `data_range` como Y y `cats_range` como X; `pie` ignora los títulos de eje.
- **Carga en memoria:** openpyxl carga el libro entero; para libros muy grandes considera ingerir a DuckDB (`excel_to_duckdb`) y consultar allí.
- **`upsert_xlsx_sheet` es la vía para datos editados por humanos:** si una persona rellena columnas a mano, pásalas en `preserve_cols` para que un re-volcado no las pise.
## Fronteras
- NO es una herramienta de BI ni de dashboards. Para visualización interactiva/compartida: Metabase, Evidence (sobre DuckDB) o gráficos embebidos con `add_xlsx_chart` para el caso "todo en el .xlsx".
- El análisis pesado (agregaciones, joins, histórico) NO se hace en Excel: ingiere a DuckDB con `excel_to_duckdb` y usa el grupo `duckdb`.
- NO cubre `.csv` de entrada con encodings legacy — eso es `safe_read_csv_fallback_py_core`.
## Relación con otros grupos
- `duckdb``excel_to_duckdb` es el puente de entrada; el motor analítico vive allí.
- `postgres` — la salida hacia BI pasa por `duckdb_to_postgres` (grupo `duckdb`/`postgres`).
- `metabase` — consume los datos una vez en Postgres.
+61
View File
@@ -0,0 +1,61 @@
# Capability: postgres
CRUD de PostgreSQL desde el registry. Las funciones Python (psycopg2) reciben un `dsn: str`, son impuras y devuelven un dict `{status:'ok'|'error', ...}` sin lanzar (mismo estilo que el grupo `duckdb`); la función Go (`postgres_open`) abre un `*sql.DB` desde parámetros individuales.
Postgres es la **capa que sirve datos a las herramientas de BI** del stack (`Excel → DuckDB → Postgres → visualización`). Metabase, Grafana y Superset NO hablan DuckDB de forma nativa, pero todas hablan PostgreSQL: por eso el motor analítico de trabajo es DuckDB y, cuando un dashboard tiene que consumir esos datos, se sincronizan a Postgres con `duckdb_to_postgres` (grupo `duckdb`).
## Funciones
| ID | Firma | Que hace |
|---|---|---|
| `postgres_open_go_infra` | `PostgresOpen(host, port, user, password, dbname, sslmode) (*sql.DB, error)` | Conecta a PostgreSQL desde Go construyendo el DSN. `sslmode` por defecto `disable`. |
| `pg_query_py_infra` | `pg_query(dsn, sql, params=None, max_rows=10000) -> dict` | SELECT read-only (`SET TRANSACTION READ ONLY`) con `RealDictCursor`. Devuelve `{status, columns, rows, row_count, truncated}`. Normaliza tipos no JSON (date/datetime→ISO, Decimal→float, bytes→base64, UUID→str). Espejo de `duckdb_query_readonly`. Valores por `%s`. |
| `pg_insert_rows_py_infra` | `pg_insert_rows(dsn, table, rows, add_snapshot_date=True) -> int` | INSERT append-only en lote (`execute_values`). Deriva columnas de las claves. Opcional `snapshot_date = date.today()`. Retorna nº de filas. |
| `pg_upsert_py_infra` | `pg_upsert(dsn, table, rows, key_cols, update_cols=None) -> dict` | UPSERT idempotente `INSERT ... ON CONFLICT (key_cols) DO UPDATE SET col=EXCLUDED.col`. `update_cols` = ownership selectivo (las no listadas conservan su valor); `[]` = DO NOTHING. Devuelve `{status, inserted, updated}`. `key_cols` deben tener PK/UNIQUE. Espejo de `duckdb_upsert`. |
| `pg_create_table_from_rows_py_infra` | `pg_create_table_from_rows(dsn, table, rows, primary_key=None) -> dict` | `CREATE TABLE IF NOT EXISTS` infiriendo columnas y tipos desde los valores (bool→BOOLEAN, int→BIGINT, float→DOUBLE PRECISION, datetime→TIMESTAMP, date→DATE, resto→TEXT). Idempotente. Devuelve `{status, created, table, columns}`. |
| `pg_list_tables_py_infra` | `pg_list_tables(dsn, schema='public') -> dict` | Introspección read-only: tablas base con sus columnas vía `information_schema`. Devuelve `{status, schema, tables:[{name, columns:[{name,type,nullable}]}]}`. |
| `pg_apply_sql_py_infra` | `pg_apply_sql(dsn, sql_path) -> int` | Ejecuta un archivo `.sql` completo (multi-statement, una transacción). Para migraciones idempotentes (`IF NOT EXISTS`). |
Relacionadas (otros grupos): `duckdb_to_postgres_py_pipelines` (sincroniza una tabla DuckDB a Postgres) e `init_metabase_go_infra` (despliega el stack Metabase + Postgres en Docker).
## Ejemplo canónico
Crear una tabla inferida, hacer upsert idempotente y consultar (DSN desde `pass`):
```bash
cd /home/enmanuel/fn_registry
DSN="postgresql://captacion:$(pass captacion/postgres | head -1)@localhost:5433/trends"
python/.venv/bin/python3 - "$DSN" <<'PYEOF'
import sys
sys.path.insert(0, "python/functions")
from infra import pg_create_table_from_rows, pg_upsert, pg_query
dsn = sys.argv[1]
rows = [{"mes": "2026-01", "total": 12500.5}, {"mes": "2026-02", "total": 15800.75}]
pg_create_table_from_rows(dsn, "demo_kpi", rows, primary_key=["mes"])
print(pg_upsert(dsn, "demo_kpi", rows, key_cols=["mes"])) # inserted/updated
print(pg_upsert(dsn, "demo_kpi", rows, key_cols=["mes"])) # idempotente: 0 inserts
print(pg_query(dsn, "SELECT * FROM demo_kpi ORDER BY mes")["rows"])
PYEOF
```
## Gotchas del grupo
- **El DSN lleva credenciales — nunca hardcodear.** Resuélvelo desde `pass` (ej. `pass captacion/postgres`: L1 = password, resto `user/host/port/datadb`). No imprimas el DSN en logs.
- **`pg_query`/`pg_list_tables` son read-only por convención** (`SET TRANSACTION READ ONLY` + rollback), protegen la base pero NO son sandbox; los identificadores (tabla/schema) NO se parametrizan — los valores sí (`%s`). Las funciones validan identificadores con `^[A-Za-z_][A-Za-z0-9_]*$`.
- **`pg_upsert` cuenta insert vs update con el pseudo-columna `xmax`** (`RETURNING (xmax = 0)`). Fiable en el caso normal (single-writer, sin triggers raros). Con `update_cols=[]` (DO NOTHING) las filas en conflicto no se devuelven, así que solo se cuentan las nuevas. BEFORE-triggers / REPLICA IDENTITY pueden desviar el conteo.
- **`pg_create_table_from_rows` no reconcilia schema:** si la tabla ya existe, `columns` reporta los tipos inferidos de las filas, no los reales. Inferencia best-effort sin NUMERIC/escala — para dinero define el schema a mano con `pg_apply_sql`.
- **`pg_insert_rows` y `pg_apply_sql` lanzan en error** (no devuelven dict); envuélvelas si compones.
## Fronteras
- NO es el motor analítico del stack — ese es DuckDB (columnar, lee CSV/Parquet/Excel nativo). Postgres es el destino para BI.
- NO dibuja dashboards: eso es Metabase / Grafana / Evidence leyendo de Postgres.
- NO cubre PostGIS más allá de `osm2pgsql_ingest_py_infra` (geo, aparte).
## Relación con otros grupos
- `duckdb``duckdb_to_postgres` es el puente de entrada de datos a esta capa.
- `metabase` — registra la base con `metabase_add_database(engine='postgres', ...)` y consume las tablas.
- `excel` — el origen de los datos suele ser un `.xlsx` ingerido por `excel_to_duckdb`.