"""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 = 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))