diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 464df20a..b7b92e11 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -252,16 +252,26 @@ analysis/ ### Crear un analisis nuevo -```bash -# Basico -fn run init_jupyter_analysis finanzas +Un solo comando deja todo listo: carpetas, venv, paquetes, launcher, MCP, kernel startup, `analysis.md` con frontmatter y, si va en un proyecto, `fn index` final. -# Con paquetes extra +```bash +# Analisis suelto (analysis/{nombre}/) +fn run init_jupyter_analysis finanzas fn run init_jupyter_analysis ml scikit-learn torch -fn run init_jupyter_analysis duckdb polars duckdb + +# Analisis dentro de un proyecto (projects/{proyecto}/analysis/{nombre}/) +fn run init_jupyter_analysis --project aurgi sale_prices --desc "Comprobacion precios" +fn run init_jupyter_analysis --project fn_monitoring coverage polars --tags "monitoring,coverage" ``` -El pipeline `init_jupyter_analysis_bash_pipelines` compone 8 funciones atomicas del registry. +Flags del pipeline: +- `--project ` — crea el analisis dentro de `projects/{nombre}/analysis/` y ejecuta `fn index` al final. El proyecto debe existir (`projects/{nombre}/project.md`). +- `--desc "..."` — descripcion que se escribe en el frontmatter de `analysis.md`. +- `--tags "a,b,c"` — tags CSV que se escriben en el frontmatter. + +**NUNCA** uses `mv` para mover un analisis de `analysis/` a `projects/{proyecto}/analysis/` despues de crearlo. Al mover, el `.venv/bin/activate` queda con el path antiguo hardcodeado y el launcher falla con `ERROR: jupyter-collaboration no esta instalado`. Si esto pasa: `rm -rf .venv && uv sync` dentro del directorio nuevo. La forma correcta es siempre crear con `--project` desde el inicio. + +El pipeline `init_jupyter_analysis_bash_pipelines` (v1.1.0) compone 9 funciones atomicas del registry. ### Usar un analisis diff --git a/.claude/rules/projects.md b/.claude/rules/projects.md index 1835f238..8987c75c 100644 --- a/.claude/rules/projects.md +++ b/.claude/rules/projects.md @@ -53,17 +53,15 @@ mkdir -p ~/vaults/{vault_name}/{raw,processed,exports} ln -s ~/vaults/{vault_name} projects/{nombre}/vaults/{vault_name} # Crear vault.yaml con la entrada -# 4. Crear analysis dentro del proyecto -fn run init_jupyter_analysis {nombre_analysis} [paquetes...] -mv analysis/{nombre_analysis} projects/{nombre}/analysis/ -# Crear analysis.md con dir_path correcto -# Regenerar launcher y kernel startup: -source bash/functions/infra/write_jupyter_launcher.sh && write_jupyter_launcher projects/{nombre}/analysis/{tema} -source bash/functions/infra/write_jupyter_registry_kernel.sh && write_jupyter_registry_kernel projects/{nombre}/analysis/{tema} +# 4. Crear analysis dentro del proyecto (un solo comando; ya indexa) +fn run init_jupyter_analysis --project {nombre} {nombre_analysis} --desc "..." [paquetes...] -# 5. Indexar -fn index +# 5. Verificar fn show {nombre} # verifica el project y sus componentes + +# NUNCA: crear el analisis en analysis/ y luego mv al proyecto. +# Al mover se rompe el .venv (paths hardcodeados en activate). +# Si ya te paso: cd projects/{nombre}/analysis/{tema} && rm -rf .venv && uv sync ``` ### Consultas utiles diff --git a/bash/functions/infra/write_analysis_md.md b/bash/functions/infra/write_analysis_md.md new file mode 100644 index 00000000..b82c0b6a --- /dev/null +++ b/bash/functions/infra/write_analysis_md.md @@ -0,0 +1,39 @@ +--- +id: write_analysis_md_bash_infra +name: write_analysis_md +kind: function +lang: bash +domain: infra +version: 1.0.0 +purity: impure +signature: "write_analysis_md(analysis_dir: string, name: string, description: string, tags_csv: string) -> string" +description: "Genera un archivo analysis.md con frontmatter valido para el registry. Calcula dir_path relativo a FN_REGISTRY_ROOT (o lo deduce buscando registry.db hacia arriba). Acepta tags como CSV." +tags: [analysis, frontmatter, registry, markdown] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +example: "source write_analysis_md.sh && write_analysis_md projects/aurgi/analysis/ventas ventas 'Analisis de ventas' 'aurgi,ventas'" +tested: false +file_path: "bash/functions/infra/write_analysis_md.sh" +params: + - name: analysis_dir + desc: "Directorio del analisis donde se escribira analysis.md" + - name: name + desc: "Nombre del analisis (se usa en frontmatter name)" + - name: description + desc: "Descripcion breve del analisis" + - name: tags_csv + desc: "Tags separados por coma (opcional)" +output: "Ruta absoluta del analysis.md creado" +--- + +## Notas + +Forma parte del workflow de creacion rapida de analyses dentro de proyectos. + +Requiere que `analysis_dir` exista fisicamente antes de llamar (para resolver path absoluto). Normalmente se llama dentro del pipeline `init_jupyter_analysis` despues de crear la estructura de carpetas. + +El `dir_path` del frontmatter debe ser relativo a la raiz del registry para que `fn index` lo enlace correctamente al `project_id` si esta bajo `projects/{nombre}/analysis/`. diff --git a/bash/functions/infra/write_analysis_md.sh b/bash/functions/infra/write_analysis_md.sh new file mode 100644 index 00000000..a55ed5b0 --- /dev/null +++ b/bash/functions/infra/write_analysis_md.sh @@ -0,0 +1,67 @@ +# write_analysis_md +# ----------------- +# Genera un archivo analysis.md con frontmatter valido para el registry. +# El dir_path se calcula relativo a FN_REGISTRY_ROOT. +# +# USO (sourced): +# source write_analysis_md.sh +# write_analysis_md [tags_csv] +# +# EJEMPLOS: +# write_analysis_md projects/aurgi/analysis/sale_prices sale_prices "Comprobacion precios" +# write_analysis_md analysis/finanzas finanzas "Exploracion gastos" "finanzas,personal" + +write_analysis_md() { + local analysis_dir="${1:-}" + local name="${2:-}" + local description="${3:-}" + local tags_csv="${4:-}" + + if [ -z "$analysis_dir" ] || [ -z "$name" ]; then + echo "Uso: write_analysis_md [tags_csv]" >&2 + return 1 + fi + + # dir_path relativo a FN_REGISTRY_ROOT + local registry_root="${FN_REGISTRY_ROOT:-}" + if [ -z "$registry_root" ]; then + # Intenta deducirlo: buscar registry.db hacia arriba + local probe="$(cd "$analysis_dir" && pwd)" + while [ "$probe" != "/" ] && [ ! -f "$probe/registry.db" ]; do + probe="$(dirname "$probe")" + done + registry_root="$probe" + fi + + local abs_dir="$(cd "$analysis_dir" && pwd)" + local rel_dir="${abs_dir#${registry_root}/}" + + # Construir array YAML de tags + local tags_yaml="[]" + if [ -n "$tags_csv" ]; then + tags_yaml="[$(echo "$tags_csv" | sed 's/,/, /g')]" + fi + + local md_path="${analysis_dir}/analysis.md" + cat > "$md_path" << EOF +--- +name: ${name} +lang: py +domain: datascience +description: "${description}" +tags: ${tags_yaml} +uses_functions: [] +uses_types: [] +framework: "jupyterlab" +entry_point: "notebooks/main.ipynb" +dir_path: "${rel_dir}" +repo_url: "" +--- + +## Notas + +${description} +EOF + + echo "$md_path" +} diff --git a/bash/functions/pipelines/init_jupyter_analysis.md b/bash/functions/pipelines/init_jupyter_analysis.md index 482612db..85a6df50 100644 --- a/bash/functions/pipelines/init_jupyter_analysis.md +++ b/bash/functions/pipelines/init_jupyter_analysis.md @@ -3,10 +3,10 @@ name: init_jupyter_analysis kind: pipeline lang: bash domain: pipelines -version: "1.0.0" +version: "1.1.0" purity: impure -signature: "init_jupyter_analysis(nombre: string, [...paquetes_extra: string]) -> void" -description: "Inicializa un analisis Jupyter completo en analysis/{nombre}/ con venv, paquetes, launcher, MCP y reglas para agentes Claude. Acepta paquetes extra opcionales." +signature: "init_jupyter_analysis([--project

] [--desc ] [--tags ], nombre: string, [...paquetes_extra: string]) -> void" +description: "Inicializa un analisis Jupyter completo con venv, paquetes, launcher, MCP, reglas Claude, kernel startup y analysis.md. Por defecto crea en analysis/{nombre}/; con --project crea en projects/{proyecto}/analysis/{nombre}/ y ejecuta fn index al final." tags: [jupyter, analysis, setup, pipeline, bash, launcher] uses_functions: - assert_command_exists_bash_shell @@ -17,6 +17,7 @@ uses_functions: - write_mcp_jupyter_config_bash_infra - write_claude_jupyter_rules_bash_infra - write_jupyter_registry_kernel_bash_infra + - write_analysis_md_bash_infra uses_types: [] returns: [] returns_optional: false @@ -27,7 +28,13 @@ params: desc: "nombre del análisis a crear" - name: paquetes_extra desc: "paquetes Python adicionales a instalar (variadic, opcional)" -output: "sin salida directa; estructura completa en analysis/{nombre}/" + - name: "--project" + desc: "nombre del proyecto bajo projects/ donde crear el analisis (opcional); si se omite, se crea en analysis/ raiz" + - name: "--desc" + desc: "descripcion breve del analisis (se escribe en analysis.md)" + - name: "--tags" + desc: "tags CSV que se escriben en analysis.md (opcional)" +output: "sin salida directa; estructura completa + analysis.md en el destino. Con --project, ejecuta fn index para registrar." tested: false tests: [] test_file_path: "" @@ -37,28 +44,30 @@ file_path: "bash/functions/pipelines/init_jupyter_analysis.sh" ## Ejemplo ```bash -# Analisis basico -./init_jupyter_analysis.sh finanzas - -# Con paquetes extra -./init_jupyter_analysis.sh duckdb polars duckdb -./init_jupyter_analysis.sh ml scikit-learn torch - -# Via fn run +# Analisis suelto en analysis/ fn run init_jupyter_analysis finanzas fn run init_jupyter_analysis ml scikit-learn torch + +# Analisis dentro de un proyecto (un solo comando, todo resuelto) +fn run init_jupyter_analysis --project aurgi sale_prices --desc "Comprobacion precios" +fn run init_jupyter_analysis --project aurgi ventas polars --tags "aurgi,ventas" ``` ## Flujo 1. `assert_command_exists` — verifica que uv o python3 estan disponibles -2. Crea estructura `analysis/{nombre}/notebooks/` y `analysis/{nombre}/data/` -3. `init_uv_venv` — crea venv en `analysis/{nombre}/.venv/` +2. Crea estructura `{destino}/notebooks/` y `{destino}/data/` +3. `init_uv_venv` — crea venv en `{destino}/.venv/` 4. `uv_add_packages` — instala jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib + extras 5. `write_jupyter_launcher` — genera `run-jupyter-lab.sh` con modo colaborativo 6. `find_free_port` + `write_mcp_jupyter_config` — detecta puerto libre y genera `.mcp.json` 7. `write_claude_jupyter_rules` — genera `.claude/CLAUDE.md` con reglas de agente 8. `write_jupyter_registry_kernel` — genera IPython startup con `fn_query`, `fn_search`, `fn_code` y acceso a `python/functions/` +9. `write_analysis_md` — genera `analysis.md` con frontmatter y `dir_path` correcto + +Si se paso `--project`, al final ejecuta `fn index` para registrar el analisis en registry.db con `project_id` correcto. + +Con `--project`, el destino es `projects/{proyecto}/analysis/{nombre}/` (requiere que `projects/{proyecto}/project.md` exista); sin el, es `analysis/{nombre}/`. ## Notas diff --git a/bash/functions/pipelines/init_jupyter_analysis.sh b/bash/functions/pipelines/init_jupyter_analysis.sh index a977103b..e6259c1d 100644 --- a/bash/functions/pipelines/init_jupyter_analysis.sh +++ b/bash/functions/pipelines/init_jupyter_analysis.sh @@ -1,18 +1,29 @@ #!/usr/bin/env bash # init_jupyter_analysis # ---------------------- -# Inicializa un analisis Jupyter completo en analysis/{nombre}/. +# Inicializa un analisis Jupyter completo. +# +# Dos modos de uso: +# Suelto: analysis/{nombre}/ +# Proyecto: projects/{proyecto}/analysis/{nombre}/ (con --project ) +# +# En modo proyecto, tambien genera analysis.md con frontmatter correcto +# y ejecuta `fn index` al final para que el analisis quede registrado. +# # Compone: assert_command_exists + find_free_port + init_uv_venv + # uv_add_packages + write_jupyter_launcher + # write_mcp_jupyter_config + write_claude_jupyter_rules + -# write_jupyter_registry_kernel +# write_jupyter_registry_kernel + write_analysis_md # # USO: # ./init_jupyter_analysis.sh [paquetes_extra...] +# ./init_jupyter_analysis.sh --project [paquetes_extra...] +# ./init_jupyter_analysis.sh --project [paquetes_extra...] +# ./init_jupyter_analysis.sh [paquetes...] --desc "descripcion" # # EJEMPLOS: # ./init_jupyter_analysis.sh finanzas -# ./init_jupyter_analysis.sh duckdb polars duckdb +# ./init_jupyter_analysis.sh --project aurgi sale_prices --desc "Comprobar precios" # ./init_jupyter_analysis.sh ml scikit-learn torch set -euo pipefail @@ -29,49 +40,94 @@ source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_launcher.sh" source "$REGISTRY_ROOT/bash/functions/infra/write_mcp_jupyter_config.sh" source "$REGISTRY_ROOT/bash/functions/infra/write_claude_jupyter_rules.sh" source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_registry_kernel.sh" +source "$REGISTRY_ROOT/bash/functions/infra/write_analysis_md.sh" -# ── Argumentos ────────────────────────────────────────────── +# ── Parsing de argumentos (flags mezclados con posicionales) ─ + +PROJECT="" +NOMBRE="" +DESC="" +TAGS="" +EXTRA_PACKAGES=() + +while [ $# -gt 0 ]; do + case "$1" in + --project) + PROJECT="$2"; shift 2 ;; + --desc|--description) + DESC="$2"; shift 2 ;; + --tags) + TAGS="$2"; shift 2 ;; + -h|--help) + grep "^#" "$0" | sed 's/^# \?//' ; exit 0 ;; + -*) + echo "Flag desconocido: $1" >&2 ; exit 1 ;; + *) + if [ -z "$NOMBRE" ]; then + NOMBRE="$1" + else + EXTRA_PACKAGES+=("$1") + fi + shift ;; + esac +done -NOMBRE="${1:-}" if [ -z "$NOMBRE" ]; then - echo "Uso: $0 [paquetes_extra...]" >&2 - echo " Ejemplo: $0 finanzas polars" >&2 + echo "Uso: $0 [--project ] [paquetes_extra...]" >&2 + echo " Ejemplo: $0 --project aurgi sale_prices --desc 'Comprobar precios'" >&2 exit 1 fi -shift -EXTRA_PACKAGES=("$@") -ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}" +# ── Resolver directorio destino ────────────────────────────── + +if [ -n "$PROJECT" ]; then + PROJECT_DIR="${REGISTRY_ROOT}/projects/${PROJECT}" + if [ ! -f "${PROJECT_DIR}/project.md" ]; then + echo "ERROR: El proyecto '${PROJECT}' no existe en projects/${PROJECT}/" >&2 + echo " Creao primero con 'fn add -k project' o manualmente." >&2 + exit 1 + fi + ANALYSIS_DIR="${PROJECT_DIR}/analysis/${NOMBRE}" +else + ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}" +fi + +if [ -z "$DESC" ]; then + DESC="Analisis ${NOMBRE}" +fi echo "" echo "════════════════════════════════════════════════════════════" echo " INIT JUPYTER ANALYSIS: ${NOMBRE}" +if [ -n "$PROJECT" ]; then + echo " Proyecto: ${PROJECT}" +fi echo " Directorio: ${ANALYSIS_DIR}" echo "════════════════════════════════════════════════════════════" echo "" # ── 1. Verificar herramientas ─────────────────────────────── -echo "[1/8] Verificando herramientas..." +echo "[1/9] Verificando herramientas..." assert_command_exists uv || assert_command_exists python3 echo " OK" # ── 2. Crear estructura de carpetas ───────────────────────── -echo "[2/8] Creando estructura..." +echo "[2/9] Creando estructura..." mkdir -p "$ANALYSIS_DIR/notebooks" "$ANALYSIS_DIR/data" echo " ${ANALYSIS_DIR}/notebooks/" echo " ${ANALYSIS_DIR}/data/" # ── 3. Crear venv ─────────────────────────────────────────── -echo "[3/8] Inicializando venv..." +echo "[3/9] Inicializando venv..." venv_path=$(init_uv_venv "$ANALYSIS_DIR") echo " $venv_path" # ── 4. Instalar paquetes ──────────────────────────────────── -echo "[4/8] Instalando paquetes..." +echo "[4/9] Instalando paquetes..." BASE_PACKAGES=(jupyter jupyterlab jupyter-collaboration jupyter-mcp-server pandas numpy matplotlib) ALL_PACKAGES=("${BASE_PACKAGES[@]}" "${EXTRA_PACKAGES[@]}") uv_add_packages "$ANALYSIS_DIR" "${ALL_PACKAGES[@]}" @@ -79,29 +135,46 @@ echo " Instalados: ${ALL_PACKAGES[*]}" # ── 5. Generar launcher ───────────────────────────────────── -echo "[5/8] Generando launcher..." +echo "[5/9] Generando launcher..." launcher=$(write_jupyter_launcher "$ANALYSIS_DIR") echo " $launcher" # ── 6. Configurar MCP ─────────────────────────────────────── -echo "[6/8] Configurando MCP..." +echo "[6/9] Configurando MCP..." port=$(find_free_port 8888 8899) mcp_config=$(write_mcp_jupyter_config "$ANALYSIS_DIR" "$port") echo " $mcp_config (puerto: $port)" # ── 7. Reglas para agentes ────────────────────────────────── -echo "[7/8] Escribiendo reglas Claude..." +echo "[7/9] Escribiendo reglas Claude..." rules=$(write_claude_jupyter_rules "$ANALYSIS_DIR") echo " $rules" # ── 8. Kernel startup con acceso al registry ──────────────── -echo "[8/8] Configurando kernel con acceso al registry..." +echo "[8/9] Configurando kernel con acceso al registry..." kernel_startup=$(write_jupyter_registry_kernel "$ANALYSIS_DIR") echo " $kernel_startup" +# ── 9. analysis.md (+ fn index si es proyecto) ────────────── + +echo "[9/9] Escribiendo analysis.md..." +export FN_REGISTRY_ROOT="$REGISTRY_ROOT" +md_path=$(write_analysis_md "$ANALYSIS_DIR" "$NOMBRE" "$DESC" "$TAGS") +echo " $md_path" + +if [ -n "$PROJECT" ]; then + echo "" + echo " Indexando registry..." + if [ -x "${REGISTRY_ROOT}/fn" ]; then + ( cd "$REGISTRY_ROOT" && ./fn index 2>&1 | tail -3 ) + else + echo " WARN: binario 'fn' no encontrado en ${REGISTRY_ROOT}. Ejecuta 'fn index' manualmente." + fi +fi + # ── Resumen ───────────────────────────────────────────────── echo "" diff --git a/python/functions/notebook/jupyter_write.py b/python/functions/notebook/jupyter_write.py index 751aadfa..2ee0da78 100644 --- a/python/functions/notebook/jupyter_write.py +++ b/python/functions/notebook/jupyter_write.py @@ -36,12 +36,16 @@ def _resolve_collab_username(server_url: str, token: str) -> str: def _notebook_exists(notebook_path: str, server_url: str, token: str) -> bool: - """Comprueba si un notebook existe en el servidor Jupyter via HEAD /api/contents.""" + """Comprueba si un notebook existe en el servidor Jupyter via GET /api/contents. + + Usa GET con ?content=0 (metadata only). HEAD no es soportado universalmente + (Jupyter 4.x devuelve 405 Method Not Allowed en /api/contents/{path}). + """ headers = {"Accept": "application/json"} if token: headers["Authorization"] = f"token {token}" - check_url = f"{server_url}/api/contents/{notebook_path}" - req = Request(check_url, headers=headers, method="HEAD") + check_url = f"{server_url}/api/contents/{notebook_path}?content=0" + req = Request(check_url, headers=headers, method="GET") try: with urlopen(req, timeout=5): return True @@ -340,9 +344,9 @@ def jupyter_create_notebook( if token: headers["Authorization"] = f"token {token}" - # Verificar si ya existe (HEAD request) + # Verificar si ya existe (GET con content=0; HEAD no esta soportado en Jupyter 4.x) check_url = f"{server_url}/api/contents/{notebook_path}" - check_req = Request(check_url, headers=headers, method="HEAD") + check_req = Request(f"{check_url}?content=0", headers=headers, method="GET") already_exists = False try: with urlopen(check_req, timeout=5):