---
name: ingest_market_trends_headless
kind: pipeline
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "def ingest_market_trends_headless(sources: str = '', port: int = 9334, profile_dir: str = '') -> dict"
description: "Ingesta de las fuentes CDP de tendencias (AliExpress / Amazon movers / saturación Amazon) en un Chrome headless AISLADO con perfil dedicado, lanzándolo y cerrándolo en cada corrida. Evita abrir pestañas en el navegador diario del usuario (chromium-personal, CDP 9222). Wrapper de ingest_market_trends. Proyecto captacion_clientes."
tags: [market-intel, headless, cdp, dropship, scraper, ingest]
uses_functions: [ingest_market_trends_py_pipelines]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/pipelines/ingest_market_trends_headless.py"
params:
  - name: sources
    desc: "Fuentes CDP separadas por coma. Vacío -> las 3 del proyecto (aliexpress_cdp,amazon_movers_cdp,amazon_saturation_cdp). Un source que falle no aborta el resto."
  - name: port
    desc: "Puerto de remote-debugging del Chrome headless aislado. DEBE coincidir con el `port` de los bloques *_cdp en sources.json (allí ya está a 9334). Default 9334."
  - name: profile_dir
    desc: "user-data-dir dedicado del Chrome aislado. Vacío -> ~/.config/fn_scrape_chrome (se crea si no existe). Perfil persistente entre corridas."
output: "dict {status: 'ok'|'error', port, profile_dir, launched: bool, closed: bool, results: [{source, scraped, inserted} | {source, error}]}. Nunca lanza excepción al caller: el finally cierra siempre la instancia que lanzó."
---

## Ejemplo

```bash
# Las 3 fuentes CDP en Chrome headless aislado (lanzar -> scrape -> cerrar).
# OJO: fn run pasa los args POSICIONALES (no flags --), en el orden sources, port, profile_dir.
fn run ingest_market_trends_headless
# -> {"status":"ok","port":9334,"profile_dir":"/home/<user>/.config/fn_scrape_chrome",
#     "launched":true,"closed":true,
#     "results":[{"source":"aliexpress_cdp",...},{"source":"amazon_movers_cdp",...},
#                {"source":"amazon_saturation_cdp","scraped":20,"inserted":20}]}

# Solo la fuente de saturación de Amazon (la validada headless sin login):
fn run ingest_market_trends_headless amazon_saturation_cdp
# -> {"status":"ok","port":9334,"launched":true,"closed":true,
#     "results":[{"source":"amazon_saturation_cdp","scraped":5,"inserted":5}]}

# Puerto/perfil custom (args posicionales: sources, port, profile_dir):
fn run ingest_market_trends_headless amazon_saturation_cdp 9340 ~/.config/otro_scrape
```

Invocación directa del módulo (acepta flags `--sources`/`--port`/`--profile-dir`):

```bash
python/.venv/bin/python3 python/functions/pipelines/ingest_market_trends_headless.py \
    --sources amazon_saturation_cdp
```

## Cuando usarla

Úsala para la ingesta diaria/programada (dag_engine) de las fuentes CDP del proyecto
captacion_clientes cuando NO quieras que el scraping abra pestañas en tu navegador diario.
Levanta su propio Chromium headless con perfil dedicado y lo cierra al terminar — el
navegador personal (`chromium-personal`, CDP 9222) queda intacto. Es el reemplazo de llamar
`ingest_market_trends <source_cdp>` a pelo (que usaría el 9222 con sesión interactiva).

## Gotchas

- **Impura: lanza y mata Chrome.** Arranca un Chromium headless vía `systemd-run --user`
  (scope `fnscrape_dag_<port>`); si `systemd-run` no está, cae a `subprocess.Popen` con
  grupo de proceso propio. Lo lanzar con `exec` directo desde el agente da **exit-144** — por
  eso systemd-run. En el `finally` siempre cierra lo que lanzó (`systemctl --user stop` del
  scope/service + respaldo `pkill -f "user-data-dir=<perfil>"`) y verifica con un GET final
  que el puerto ya no responde (`closed`).
- **Perfil dedicado persistente.** `~/.config/fn_scrape_chrome` sobrevive entre corridas
  (cookies/cache del scraping). No se borra. Bórralo a mano si quieres sesión limpia.
- **Reutiliza CDP existente.** Si el puerto ya responde al arrancar, NO lanza otro Chrome:
  reutiliza el vivo y `launched=False` + `closed=False` (no cierra algo que no abrió).
- **Puerto = el del config.** Los bloques `*_cdp` de `sources.json` deben tener `port`
  igual al `port` de este wrapper (ya a 9334). `ingest_market_trends(source)` lee el puerto
  del config, así reutiliza la instancia que este wrapper levantó. No se le pasa el puerto.
- **AliExpress headless puede pedir captcha.** Sin login, `aliexpress_cdp` puede devolver
  `status: captcha` (0 filas) — es esperado, no es bug del wrapper. El lifecycle
  (launched -> scrape -> closed) se valida mejor con `amazon_saturation_cdp`.
- **DSN Postgres.** La ingesta necesita el DSN de `trends` (CAPTACION_DSN / .env / pass
  captacion/postgres). Si falla la resolución, esa fuente cae en `{source, error}` pero el
  Chrome igual se cierra.
