feat(infra): auto-commit con 56 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:22:55 +02:00
parent c1071a82b3
commit 32c7336bf6
56 changed files with 5307 additions and 100 deletions
@@ -0,0 +1,100 @@
"""Ejecuta un SELECT contra el service osint_db (DuckDB, 127.0.0.1:8771) por HTTP.
Funcion impura: hace un POST a ``{base_url}/api/query`` con el cuerpo JSON
``{"sql": sql}`` y devuelve el resultado sin lanzar excepciones, siguiendo el estilo
de pg_query / duckdb_query_readonly del registry: {status:'ok', ...} en exito y
{status:'error', error:str} en fallo.
El osint_db es un FastAPI single-writer sobre DuckDB que es la fuente de verdad del
proyecto osint. Su endpoint /api/query es estrictamente read-only (abre una conexion
DuckDB read_only separada) y responde SIEMPRE con HTTP 200; el status real del
dominio viaja en el cuerpo ({status, columns, rows, row_count, truncated} en exito,
o {status:'error', error}). Esta funcion reenvia ese cuerpo tal cual cuando es ok y
normaliza los errores de red (service caido, timeout, conexion rechazada) a un
{status:'error', ...} con un mensaje claro, para no tumbar al llamante.
Solo stdlib (urllib, json): el wrapper es transporte puro, no reimplementa la logica
del osint_db ni anade dependencias de runtime.
"""
import json
import urllib.error
import urllib.request
def query_osint_db(
sql: str,
base_url: str = "http://127.0.0.1:8771",
timeout: int = 30,
) -> dict:
"""Ejecuta un SELECT contra el service osint_db por HTTP y devuelve un dict.
Args:
sql: sentencia SQL a ejecutar. Pensada para SELECT read-only; el osint_db
la corre con una conexion DuckDB en modo solo lectura, asi que una
escritura fallara a nivel de service.
base_url: URL base del service osint_db (default
"http://127.0.0.1:8771"). Se le anade "/api/query" al hacer el POST.
timeout: timeout por peticion en segundos (default 30). El osint_db es
local (loopback): si tarda mas, mejor degradar que colgar al llamante.
Returns:
dict. En exito reenvia el cuerpo del service:
{status:'ok', columns:[str,...], rows:[{col:val, ...}, ...], row_count:int,
truncated:bool}. En error (sin lanzar): {status:'error', error:str}. Si el
service no es alcanzable (no arrancado, conexion rechazada, host caido) el
error es "osint_db service not reachable on <url>: <detalle>".
"""
url = base_url.rstrip("/") + "/api/query"
data = json.dumps({"sql": sql}).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as exc:
# El contrato del osint_db es 200 siempre; un HTTPError es anomalo. Intenta
# leer el cuerpo (puede traer {status:error,...}); si no, error claro.
try:
body = json.loads(exc.read().decode("utf-8"))
if isinstance(body, dict):
return body
except (ValueError, OSError):
pass
return {
"status": "error",
"error": f"osint_db returned HTTP {exc.code} on {url}",
}
except (urllib.error.URLError, OSError) as exc:
return {
"status": "error",
"error": f"osint_db service not reachable on {url}: {exc}",
}
try:
parsed = json.loads(raw)
except ValueError as exc:
return {
"status": "error",
"error": f"osint_db returned non-JSON response on {url}: {exc}",
}
if not isinstance(parsed, dict):
return {
"status": "error",
"error": f"osint_db returned unexpected JSON type on {url}",
}
return parsed
if __name__ == "__main__":
import sys
query = sys.argv[1] if len(sys.argv) > 1 else "SELECT COUNT(*) FROM personas"
print(json.dumps(query_osint_db(query), ensure_ascii=False, indent=2))