chore: auto-commit (799 archivos)
- .claude/CLAUDE.md - .claude/commands/subagentes.md - .claude/rules/INDEX.md - .mcp.json - bash/functions/cybersecurity/analyze_dns.md - bash/functions/cybersecurity/audit_http_headers.md - bash/functions/cybersecurity/audit_ssh_config.md - bash/functions/cybersecurity/check_firewall.md - bash/functions/cybersecurity/detect_suspicious_users.md - bash/functions/cybersecurity/encrypt_file.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "add_header_logo(fig: Figure, image: np.ndarray, x: float = 0.88, y: float = 0.905, width: float = 0.08, height: float = 0.08) -> None"
|
||||
description: "Añade un logo como axes inset en la esquina superior derecha de una figura matplotlib. Usa fig.add_axes + imshow + axis off. Útil para branding en páginas de informe PDF."
|
||||
tags: [pdf, matplotlib, logo, report, infra]
|
||||
tags: [pdf, matplotlib, logo, report, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "compress_pdf_ghostscript(pdf_path: str | Path, quality: str = 'screen') -> bool"
|
||||
description: "Comprime un PDF en disco usando Ghostscript con downsampling 96/200 dpi. Reemplaza el archivo solo si el comprimido es menor. Retorna True si comprimió, False si gs no disponible o no hubo mejora."
|
||||
tags: [pdf, ghostscript, compression, infra]
|
||||
tags: [pdf, ghostscript, compression, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "config_from_env(target_class: type) -> object"
|
||||
description: "Crea una instancia de un dataclass poblada desde variables de entorno usando field metadata (env, required, default, secret). Soporta str, int, float, bool, list. Acumula todos los errores antes de lanzar ValueError."
|
||||
tags: [config, env, dataclass, infra, python]
|
||||
tags: [config, env, dataclass, infra, python, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "dotenv_load(path: str) -> None"
|
||||
description: "Parsea un archivo .env (KEY=VALUE) e inyecta pares en os.environ. Ignora comentarios # y lineas vacias. No sobreescribe variables ya presentes. Soporta valores con comillas simples o dobles."
|
||||
tags: [dotenv, env, config, os, infra, python]
|
||||
tags: [dotenv, env, config, os, infra, python, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "email_build_html(from_addr: str, to: list[str], subject: str, body_html: str) -> EmailMessagePy"
|
||||
description: "Construye un EmailMessagePy inmutable con cuerpo HTML. El campo body_text queda vacio. Funcion pura sin efectos secundarios."
|
||||
tags: [email, html, builder, pure, python, dataclass]
|
||||
tags: [email, html, builder, pure, python, dataclass, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def get_logger(name: str = 'app') -> logging.Logger"
|
||||
description: "Devuelve un logger existente si ya tiene handlers, o lo crea con setup_logger. Util en modulos internos que no controlan la inicializacion del logger."
|
||||
tags: [logging, logger, infra, utility]
|
||||
tags: [logging, logger, infra, utility, pendiente-usar]
|
||||
uses_functions: [setup_logger_py_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "http_download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, timeout: float = 120.0, chunk_size: int = 8192) -> dict"
|
||||
description: "Descarga un archivo por HTTP en streaming (sin cargar todo en memoria). Crea directorios intermedios si no existen. Retorna dict con path, size_bytes y content_type."
|
||||
tags: [http, download, file, streaming, network, stdlib, infra]
|
||||
tags: [http, download, file, streaming, network, stdlib, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "http_get_json(url: str, headers: dict[str, str] | None = None, params: dict[str, str] | None = None, timeout: float = 30.0) -> dict"
|
||||
description: "GET request que espera JSON. Agrega Accept: application/json automaticamente. Lanza RuntimeError si status >= 400 con status code, url truncada y primeros 200 chars del body."
|
||||
tags: [http, json, get, client, network, stdlib, infra]
|
||||
tags: [http, json, get, client, network, stdlib, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "http_post_json(url: str, body: dict, headers: dict[str, str] | None = None, timeout: float = 30.0) -> dict"
|
||||
description: "POST request con body JSON. Agrega Content-Type: application/json y Accept: application/json. Lanza RuntimeError si status >= 400 con status code, url truncada y primeros 200 chars del body."
|
||||
tags: [http, json, post, client, network, stdlib, infra]
|
||||
tags: [http, json, post, client, network, stdlib, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def nominatim_reverse_geocode(lat: float, lon: float, lang: str = 'es') -> dict"
|
||||
description: "Reverse geocoding usando Nominatim (OpenStreetMap). Convierte coordenadas lat/lon a una dirección estructurada con calle, ciudad, país y otros campos normalizados."
|
||||
tags: [geocoding, nominatim, openstreetmap, osm, reverse-geocoding, geo, location, address]
|
||||
tags: [geocoding, nominatim, openstreetmap, osm, reverse-geocoding, geo, location, address, pendiente-usar]
|
||||
params:
|
||||
- name: lat
|
||||
desc: "Latitud en grados decimales (rango -90 a 90)."
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def ollama_chat(messages: list[dict], model: str = 'llama3.1:8b', base_url: str = 'http://localhost:11434', temperature: float = 0.7, max_tokens: int = 1024) -> dict"
|
||||
description: "Envía una solicitud de chat completion a Ollama (API local compatible con OpenAI). Retorna el contenido generado junto a métricas de duración y tokens."
|
||||
tags: [ollama, llm, chat, inference, local-ai, http]
|
||||
tags: [ollama, llm, chat, inference, local-ai, http, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "osm2pgsql_ingest(osm_pbf_path: str | Path, host: str = 'localhost', port: int = 5432, dbname: str = 'gis', user: str = 'geoserver', password: str = 'geoserver', style: str | None = None, ensure_hstore: bool = True) -> dict"
|
||||
description: "Ingesta un archivo .osm.pbf en PostGIS usando osm2pgsql con --create --slim --hstore --multi-geometry. Verifica osm2pgsql en PATH, opcionalmente crea extensión hstore. Retorna dict {ok, rows_loaded, stderr}."
|
||||
tags: [osm, postgis, gis, osm2pgsql, infra]
|
||||
tags: [osm, postgis, gis, osm2pgsql, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "overpass_nearby_pois(lat: float, lon: float, radius_m: int = 500, categories: list[str] | None = None) -> list[dict]"
|
||||
description: "Consulta la Overpass API (OpenStreetMap) para obtener POIs cercanos a una coordenada. Soporta 16 categorias mapeadas a tags OSM (amenity, tourism, historic, leisure, shop). Sin dependencias externas, solo stdlib."
|
||||
tags: [osm, openstreetmap, overpass, poi, geolocation, nearby, geocoding, infra, http, stdlib]
|
||||
tags: [osm, openstreetmap, overpass, poi, geolocation, nearby, geocoding, infra, http, stdlib, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_add_header_footer(doc, header_text, footer_text, page_numbers) -> PDFDoc"
|
||||
description: "Configura header y/o footer recurrente para todas las paginas del documento. Debe llamarse antes de pdf_add_page. El footer acepta {n} y {total} como placeholders para numeracion automatica."
|
||||
tags: [pdf, header, footer, pagination, builder, infra]
|
||||
tags: [pdf, header, footer, pagination, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra]
|
||||
returns: [pdf_doc_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_add_image(doc, image, x, y, width, height, keep_aspect) -> PDFDoc"
|
||||
description: "Añade una imagen PNG o JPEG al documento PDF desde un path de archivo o bytes. Calcula automaticamente la dimension faltante si solo se especifica width o height para mantener la relacion de aspecto."
|
||||
tags: [pdf, image, embed, builder, infra]
|
||||
tags: [pdf, image, embed, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra]
|
||||
returns: [pdf_doc_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_add_page(doc, orientation, width, height) -> PDFDoc"
|
||||
description: "Añade una pagina nueva al documento PDF con orientacion y tamaño configurables. Sin dimensiones personalizadas usa A4 con la orientacion indicada."
|
||||
tags: [pdf, page, layout, builder, infra]
|
||||
tags: [pdf, page, layout, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra]
|
||||
returns: [pdf_doc_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_add_table(doc, headers, rows, col_widths, header_style, row_style, border, row_height, alternate_bg) -> PDFDoc"
|
||||
description: "Añade una tabla con fila de headers y filas de datos al documento PDF. Soporta anchos de columna personalizados, estilos diferenciados para headers/datos, bordes y colores alternados en filas."
|
||||
tags: [pdf, table, data, builder, infra]
|
||||
tags: [pdf, table, data, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra, pdf_style_py_infra]
|
||||
returns: [pdf_doc_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_add_text(doc, text, style, x, y, width) -> PDFDoc"
|
||||
description: "Escribe un bloque de texto en el documento PDF con fuente, tamaño, color y alineacion del PDFStyle dado. Soporta saltos de linea y word wrap automatico."
|
||||
tags: [pdf, text, typography, builder, infra]
|
||||
tags: [pdf, text, typography, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra, pdf_style_py_infra]
|
||||
returns: [pdf_doc_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_create(title, author, subject, default_font, margin_left, margin_right, margin_top, margin_bottom) -> PDFDoc"
|
||||
description: "Inicializa un documento PDF nuevo usando fpdf2. Crea un objeto FPDF configurado con metadatos y margenes. Retorna un PDFDoc listo para añadir paginas con pdf_add_page."
|
||||
tags: [pdf, create, fpdf2, builder, infra]
|
||||
tags: [pdf, create, fpdf2, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra]
|
||||
returns: [pdf_doc_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_from_markdown(markdown_string, output_path, css, page_size) -> str"
|
||||
description: "Convierte un string Markdown a PDF via HTML + weasyprint. Incluye CSS por defecto para tipografia legible. Soporta tablas, codigo y headers. Requiere weasyprint y markdown instalados."
|
||||
tags: [pdf, markdown, conversion, weasyprint, infra]
|
||||
tags: [pdf, markdown, conversion, weasyprint, infra, pendiente-usar]
|
||||
uses_functions: [pdf_from_html_py_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_merge(pdf_paths, output_path) -> str"
|
||||
description: "Fusiona una lista de archivos PDF en un unico PDF combinado usando pypdf. Mantiene el orden de la lista. Lanza FileNotFoundError si alguno de los archivos no existe."
|
||||
tags: [pdf, merge, combine, pypdf, infra]
|
||||
tags: [pdf, merge, combine, pypdf, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def pdf_save(doc, output_path) -> str | bytes"
|
||||
description: "Serializa el documento PDF a disco o retorna los bytes en memoria. Si output_path es None retorna bytes para transmision directa (HTTP response, almacenamiento en BD). Si se especifica, escribe el archivo y retorna el path."
|
||||
tags: [pdf, save, output, builder, infra]
|
||||
tags: [pdf, save, output, builder, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [pdf_doc_py_infra]
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def powertoys_config_path(user: str | None = None) -> str"
|
||||
description: "Devuelve el path al default.json de PowerToys Keyboard Manager. Soporta WSL, Windows nativo y override via env var POWERTOYS_CONFIG."
|
||||
tags: [powertoys, keyboard, windows, wsl, config, path]
|
||||
tags: [powertoys, keyboard, windows, wsl, config, path, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def powertoys_restart() -> None"
|
||||
description: "Mata los procesos de PowerToys y los relanza para que recargue la configuracion del Keyboard Manager. Compatible con WSL via taskkill.exe y cmd.exe."
|
||||
tags: [powertoys, keyboard, windows, wsl, restart, process, taskkill]
|
||||
tags: [powertoys, keyboard, windows, wsl, restart, process, taskkill, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def powertoys_shortcut_add(keys: list[str], target_path: str, args: str = \"\", elevated: bool = False, exact_match: bool = False, start_in_dir: str = \"\", config_path: str | None = None) -> None"
|
||||
description: "Añade o reemplaza un atajo global en el config de PowerToys Keyboard Manager. Convierte nombres legibles de teclas a VK codes y escribe JSON compacto (una linea) para mantener compatibilidad con el formato de PowerToys."
|
||||
tags: [powertoys, keyboard, windows, wsl, shortcut, config, write, add]
|
||||
tags: [powertoys, keyboard, windows, wsl, shortcut, config, write, add, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def powertoys_shortcut_list(config_path: str | None = None) -> list[dict]"
|
||||
description: "Lee el default.json de PowerToys Keyboard Manager y devuelve los atajos globales con VK codes convertidos a nombres legibles."
|
||||
tags: [powertoys, keyboard, windows, wsl, shortcut, config, read]
|
||||
tags: [powertoys, keyboard, windows, wsl, shortcut, config, read, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def powertoys_shortcut_remove(keys: list[str], config_path: str | None = None) -> bool"
|
||||
description: "Elimina un atajo global del config de PowerToys Keyboard Manager por combinacion de teclas. Devuelve True si se elimino, False si no existia."
|
||||
tags: [powertoys, keyboard, windows, wsl, shortcut, config, write, remove, delete]
|
||||
tags: [powertoys, keyboard, windows, wsl, shortcut, config, write, remove, delete, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "read_file_with_encoding(path: str, encodings: list[str] | None = None) -> str"
|
||||
description: "Lee un archivo de texto intentando multiples encodings en orden hasta encontrar uno que funcione. Util para archivos de origen desconocido (Windows, Latin-1, con BOM, etc.)."
|
||||
tags: [file, encoding, io, text, utf8, latin1, cp1252, decode]
|
||||
tags: [file, encoding, io, text, utf8, latin1, cp1252, decode, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: registry_telemetry
|
||||
lang: py
|
||||
domain: infra
|
||||
version: 0.1.0
|
||||
purity: impure
|
||||
kind: function
|
||||
description: "Telemetria de invocaciones del registry desde Python. Patchea via sys.meta_path los paquetes del registry (core, finance, metabase, etc.) y registra cada llamada en call_monitor.operations.db. Activable con FN_TELEMETRY=1. Issue 0085c."
|
||||
tags: [telemetry, monitoring, registry, import-hook, pendiente-usar]
|
||||
signature: "install() -> bool"
|
||||
error_type: "error_go_core"
|
||||
returns_optional: false
|
||||
params:
|
||||
- name: FN_TELEMETRY (env)
|
||||
desc: "Si vale '1', install() registra el meta_path finder y envuelve cada modulo del registry al importarse. Si no, no-op."
|
||||
- name: FN_REGISTRY_ROOT (env)
|
||||
desc: "Override de la raiz del registry. Si no se setea, se descubre walking up desde el archivo del modulo."
|
||||
- name: CLAUDE_SESSION_ID (env)
|
||||
desc: "ID de sesion Claude Code, persistido en cada fila de calls. Vacio si no se setea."
|
||||
output: "True si el finder se instalo, False si FN_TELEMETRY != '1' o si ya estaba instalado."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
imports:
|
||||
- functools
|
||||
- importlib.abc
|
||||
- importlib.util
|
||||
- sqlite3
|
||||
- sys
|
||||
- time
|
||||
example: |
|
||||
# Activacion en heredoc o app
|
||||
import os
|
||||
os.environ["FN_TELEMETRY"] = "1"
|
||||
import sys, os.path as p
|
||||
sys.path.insert(0, p.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))
|
||||
import infra.registry_telemetry # auto-install al importarse
|
||||
from core import filter_list, map_list # automaticamente envueltos
|
||||
filter_list(lambda x: x > 0, [1, -2, 3]) # registrado en calls
|
||||
file_path: "python/functions/infra/registry_telemetry.py"
|
||||
tested: false
|
||||
notes: |
|
||||
Patron: meta_path importer (PEP 451). Cuando el agente importa `core`, `finance`, etc.,
|
||||
el finder intercepta la carga del modulo, deja que el loader real ejecute, y luego
|
||||
envuelve cada callable definido en el modulo con un decorator que mide duration
|
||||
y registra en calls (con tool_used='python_wrapper').
|
||||
|
||||
Fail-safe: si la BD no existe, INSERT falla, o cualquier excepcion ocurre durante
|
||||
el wrap, se ignora silenciosamente. NUNCA rompe el codigo cliente.
|
||||
|
||||
function_id heuristic: `{name}_py_{domain}` donde domain = nombre del paquete
|
||||
top-level (`core`, `finance`, etc.). Coincide con convencion del registry
|
||||
siempre que el paquete top-level se llame igual que el dominio.
|
||||
|
||||
Privacidad: solo function_id, duration_ms, success, error_class, type_name del error.
|
||||
NUNCA args, kwargs, return values. error_snippet truncado a 240 chars.
|
||||
---
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Telemetria de invocaciones del registry desde Python (issue 0085c).
|
||||
|
||||
Activacion explicita: env var FN_TELEMETRY=1.
|
||||
|
||||
Mecanismo: sys.meta_path importer que detecta cuando se carga un modulo bajo
|
||||
un paquete del registry (core, finance, metabase, ...) y envuelve cada
|
||||
funcion publica con un wrapper que mide duration y registra en
|
||||
projects/fn_monitoring/apps/call_monitor/operations.db.
|
||||
|
||||
Reglas:
|
||||
- No-op silencioso si BD no existe o INSERT falla. NUNCA rompe codigo cliente.
|
||||
- Solo guarda function_id, tool_used, duration_ms, success, error_class.
|
||||
NUNCA guarda argumentos concretos.
|
||||
- Idempotente: install() puede llamarse N veces sin duplicar el finder.
|
||||
- Bypass: si FN_TELEMETRY != "1", install() es no-op. La funcion sigue siendo
|
||||
importable para usos manuales (wrap_namespace).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Optional
|
||||
|
||||
_ENABLED = os.environ.get("FN_TELEMETRY", "") == "1"
|
||||
_SESSION_ID = os.environ.get("CLAUDE_SESSION_ID", "")
|
||||
_DB_PATH: Optional[str] = None
|
||||
_REGISTRY_ROOT: Optional[str] = None
|
||||
|
||||
|
||||
def _resolve_root() -> Optional[str]:
|
||||
global _REGISTRY_ROOT
|
||||
if _REGISTRY_ROOT:
|
||||
return _REGISTRY_ROOT
|
||||
env = os.environ.get("FN_REGISTRY_ROOT")
|
||||
if env and (Path(env) / "registry.db").exists():
|
||||
_REGISTRY_ROOT = env
|
||||
return env
|
||||
here = Path(__file__).resolve()
|
||||
for parent in here.parents:
|
||||
if (parent / "registry.db").exists():
|
||||
_REGISTRY_ROOT = str(parent)
|
||||
return _REGISTRY_ROOT
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_db() -> Optional[str]:
|
||||
global _DB_PATH
|
||||
if _DB_PATH:
|
||||
return _DB_PATH
|
||||
root = _resolve_root()
|
||||
if not root:
|
||||
return None
|
||||
candidate = Path(root) / "projects" / "fn_monitoring" / "apps" / "call_monitor" / "operations.db"
|
||||
if candidate.exists():
|
||||
_DB_PATH = str(candidate)
|
||||
return _DB_PATH
|
||||
return None
|
||||
|
||||
|
||||
def _registry_packages() -> set[str]:
|
||||
root = _resolve_root()
|
||||
if not root:
|
||||
return set()
|
||||
fns_dir = Path(root) / "python" / "functions"
|
||||
if not fns_dir.is_dir():
|
||||
return set()
|
||||
return {p.name for p in fns_dir.iterdir() if p.is_dir() and not p.name.startswith("_") and not p.name.startswith(".")}
|
||||
|
||||
|
||||
def _log_call(
|
||||
function_id: str,
|
||||
duration_ms: float,
|
||||
success: bool,
|
||||
error_class: str = "",
|
||||
error_snippet: str = "",
|
||||
) -> None:
|
||||
db = _resolve_db()
|
||||
if not db:
|
||||
return
|
||||
try:
|
||||
conn = sqlite3.connect(db, timeout=0.1, isolation_level=None)
|
||||
conn.execute(
|
||||
"INSERT INTO calls "
|
||||
"(session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, ts) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(strftime('%s','now') AS INTEGER))",
|
||||
(
|
||||
_SESSION_ID,
|
||||
function_id,
|
||||
"python_wrapper",
|
||||
"",
|
||||
int(duration_ms),
|
||||
1 if success else 0,
|
||||
(error_class or "")[:64],
|
||||
(error_snippet or "")[:240],
|
||||
),
|
||||
)
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass # never break user code
|
||||
|
||||
|
||||
def _wrap_callable(fn, function_id: str):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
result = fn(*args, **kwargs)
|
||||
_log_call(function_id, (time.perf_counter() - t0) * 1000, True)
|
||||
return result
|
||||
except Exception as e:
|
||||
_log_call(
|
||||
function_id,
|
||||
(time.perf_counter() - t0) * 1000,
|
||||
False,
|
||||
type(e).__name__,
|
||||
str(e),
|
||||
)
|
||||
raise
|
||||
|
||||
wrapper.__fn_telemetered__ = True # type: ignore[attr-defined]
|
||||
return wrapper
|
||||
|
||||
|
||||
def wrap_namespace(ns_globals: dict, domain: str) -> None:
|
||||
"""Wrap public callables defined in ns_globals.
|
||||
|
||||
function_id pattern: {func_name}_py_{domain}
|
||||
Only wraps callables defined in the same module (skips re-exports).
|
||||
"""
|
||||
if not _ENABLED:
|
||||
return
|
||||
module_name = ns_globals.get("__name__", "")
|
||||
for name, obj in list(ns_globals.items()):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if getattr(obj, "__fn_telemetered__", False):
|
||||
continue
|
||||
if not callable(obj):
|
||||
continue
|
||||
# Skip classes / exceptions / non-functions
|
||||
if isinstance(obj, type):
|
||||
continue
|
||||
obj_module = getattr(obj, "__module__", None)
|
||||
if obj_module and module_name and obj_module != module_name and not obj_module.startswith(module_name.split(".")[0]):
|
||||
continue
|
||||
function_id = f"{name}_py_{domain}"
|
||||
ns_globals[name] = _wrap_callable(obj, function_id)
|
||||
|
||||
|
||||
class _RegistryTelemetryLoader(importlib.abc.Loader):
|
||||
def __init__(self, real_loader, domain: str):
|
||||
self.real_loader = real_loader
|
||||
self.domain = domain
|
||||
|
||||
def create_module(self, spec):
|
||||
if hasattr(self.real_loader, "create_module"):
|
||||
return self.real_loader.create_module(spec)
|
||||
return None
|
||||
|
||||
def exec_module(self, module):
|
||||
self.real_loader.exec_module(module)
|
||||
try:
|
||||
wrap_namespace(vars(module), self.domain)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class _RegistryTelemetryFinder(importlib.abc.MetaPathFinder):
|
||||
def __init__(self, packages: Iterable[str]):
|
||||
self._packages = set(packages)
|
||||
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
top = fullname.split(".")[0]
|
||||
if top not in self._packages:
|
||||
return None
|
||||
if fullname == __name__:
|
||||
return None
|
||||
# ask the other finders for the real spec; do not recurse on self
|
||||
for finder in sys.meta_path:
|
||||
if finder is self:
|
||||
continue
|
||||
if not hasattr(finder, "find_spec"):
|
||||
continue
|
||||
try:
|
||||
spec = finder.find_spec(fullname, path, target)
|
||||
except Exception:
|
||||
continue
|
||||
if spec is None or spec.loader is None:
|
||||
continue
|
||||
spec.loader = _RegistryTelemetryLoader(spec.loader, top)
|
||||
return spec
|
||||
return None
|
||||
|
||||
|
||||
def install() -> bool:
|
||||
"""Install meta_path finder. Idempotent. Returns True if installed."""
|
||||
if not _ENABLED:
|
||||
return False
|
||||
packages = _registry_packages()
|
||||
if not packages:
|
||||
return False
|
||||
for f in sys.meta_path:
|
||||
if isinstance(f, _RegistryTelemetryFinder):
|
||||
return False
|
||||
sys.meta_path.insert(0, _RegistryTelemetryFinder(packages))
|
||||
return True
|
||||
|
||||
|
||||
if _ENABLED:
|
||||
install()
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "render_table_page_pdfpages(pdf: PdfPages, title: str, rows: list[list[str]], col_labels: list[str], max_rows: int = 28, figsize: tuple[float, float] = (11.69, 8.27), fontsize: int = 8, dpi: int = 300) -> None"
|
||||
description: "Renderiza filas como páginas de tabla paginadas en un PdfPages abierto. Usa matplotlib.pyplot.table con paginación automática por max_rows. Una página A4 landscape por chunk."
|
||||
tags: [pdf, matplotlib, table, report, infra]
|
||||
tags: [pdf, matplotlib, table, report, infra, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def safe_extract_zip(zip_path: str, dest_dir: str) -> None"
|
||||
description: "Extrae un archivo ZIP con proteccion contra Zip Slip (path traversal attack). Valida que cada archivo extraido quede dentro del directorio destino antes de extraerlo. Normaliza nombres de archivo UTF-8 antes de extraer."
|
||||
tags: [zip, extract, security, zip-slip, path-traversal, infra, io]
|
||||
tags: [zip, extract, security, zip-slip, path-traversal, infra, io, pendiente-usar]
|
||||
uses_functions: [normalize_zip_filenames_py_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def scan_directory(root: str, supported_extensions: set[str] | None = None, ignore_dirs: set[str] | None = None, include: str | None = None, exclude: str | None = None, strict: bool = False) -> DirectoryScanResult"
|
||||
description: "Recorre un arbol de directorios y clasifica cada archivo como procesable o no soportado. Util para validacion pre-importacion de directorios. Ignora dot files, symlinks, archivos vacios y directorios de build/venv/cache predefinidos. Soporta filtros include/exclude con globs."
|
||||
tags: [directory, scan, filesystem, classification, infra, walk, files]
|
||||
tags: [directory, scan, filesystem, classification, infra, walk, files, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: [classified_file_py_infra, directory_scan_result_py_infra]
|
||||
returns: [directory_scan_result_py_infra]
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "smtp_send(cfg: SMTPConfigPy, from_addr: str, to: list[str], subject: str, body_html: str = '', body_text: str = '', cc: list[str] | None = None, bcc: list[str] | None = None, attachments: list[EmailAttachmentPy] | None = None, headers: dict[str, str] | None = None) -> None"
|
||||
description: "Conecta al servidor SMTP, construye el mensaje MIME y envia el email en un solo paso. Soporta TLS directo (port 465), STARTTLS (port 587) y sin cifrado (port 25). Cierra la conexion automaticamente."
|
||||
tags: [email, smtp, send, python, smtplib, mime, tls]
|
||||
tags: [email, smtp, send, python, smtplib, mime, tls, pendiente-usar]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def vault_profile_dispatch(vault_path: str, rel_path: str, kind: str, db_path: str | None = None) -> dict"
|
||||
description: "CLI dispatcher que enruta un archivo del vault al profiler correcto segun su tipo (csv/pdf/md). Thin wrapper sobre vault_csv_profile, vault_pdf_extract y vault_knowledge_parse. Usable desde Go via os/exec para procesar archivos en bulk."
|
||||
tags: [vault, profile, dispatch, profiler, csv, pdf, md, infra]
|
||||
tags: [vault, profile, dispatch, profiler, csv, pdf, md, infra, pendiente-usar]
|
||||
uses_functions:
|
||||
- vault_csv_profile_py_datascience
|
||||
- vault_pdf_extract_py_datascience
|
||||
|
||||
Reference in New Issue
Block a user