32c7336bf6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""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))
|