"""Ejecuta una peticion HTTP y la registra en el History de Hoppscotch self-host. Doble proposito: (1) lanza la request real con `requests` resolviendo placeholders de variables y (2) opcionalmente la persiste en el UserHistory del backend Hoppscotch self-hosted via la mutation GraphQL createUserHistory, de modo que el humano la vea aparecer en vivo en la pestana History de su GUI (la GUI escucha la subscription `userHistoryCreated`). La request se ejecuta con las variables resueltas, pero lo que se guarda en el History es la request SIN resolver (con `<>`/`{{var}}` literales), igual que en la GUI: asi el humano ve la plantilla con sus variables, no los valores expandidos. La mutation esta protegida por GqlAuthGuard: el JWT de sesion viaja en la cookie `access_token`. """ import json import re import requests from infra.build_hoppscotch_collection import build_hoppscotch_collection # Hoppscotch usa la sintaxis <>; muchas plantillas tambien traen {{var}}. # Aceptamos ambas: grupo 1 = delimitador de apertura, grupo 2 = nombre de la # variable, grupo 3 = delimitador de cierre. _VAR_RE = re.compile(r"(<<|\{\{)\s*([A-Za-z0-9_]+)\s*(>>|\}\})") # Limite del cuerpo de respuesta en el output, para no devolver payloads enormes. _BODY_TRUNCATE = 5000 _HISTORY_MUTATION = ( "mutation($d:String!,$m:String!,$t:ReqType!){" " createUserHistory(reqData:$d, resMetadata:$m, reqType:$t){ id } }" ) def _resolve_placeholders(text: str, variables: dict) -> str: """Sustituye <>/{{var}} por su valor en `variables`. Si la variable no esta en `variables`, se conserva el literal tal cual (incluidos los delimitadores). Determinista y sin I/O. Args: text: cadena con (opcionales) placeholders. variables: dict name->value con los valores de sustitucion. Returns: la cadena con los placeholders conocidos resueltos. """ def repl(match: re.Match) -> str: name = match.group(2) if name in variables: return str(variables[name]) return match.group(0) return _VAR_RE.sub(repl, text) def hoppscotch_run_request( method: str, url: str, *, title: str | None = None, headers: dict | None = None, body: str | None = None, body_type: str | None = None, variables: dict | None = None, access_token: str, backend_url: str = "http://localhost:3170", record_history: bool = True, timeout_s: float = 30.0, verify_tls: bool = True, ) -> dict: """Ejecuta una request HTTP y la registra en el History de Hoppscotch. Resuelve los placeholders `<>`/`{{var}}` de la url, los headers y el body usando `variables`, lanza la peticion real con `requests`, y (si `record_history`) guarda en el UserHistory del backend self-host la request SIN resolver (para que en la GUI History se vea con las variables, igual que en el editor). Args: method: metodo HTTP (GET, POST, ...). url: endpoint, puede contener placeholders `<>`/`{{var}}`. title: nombre visible de la request en el History. None = derivar de method + path via build_hoppscotch_collection. headers: dict name->value de cabeceras. Sus values admiten placeholders. body: cuerpo de la request como texto ya serializado. Admite placeholders. body_type: tipo de cuerpo ("json"|"form"|"raw"|None). variables: dict name->value para resolver los placeholders al EJECUTAR. None = no se resuelve nada (los literales viajan tal cual). access_token: JWT de sesion (de hoppscotch_login). Viaja en la cookie `access_token`, NO en el header Authorization. Necesario para grabar en el History. backend_url: base del backend Hoppscotch self-host (sin barra final). record_history: si True y hay access_token, registra la request en el UserHistory via createUserHistory. timeout_s: timeout de la peticion HTTP en segundos. verify_tls: verificacion del certificado TLS de la request ejecutada. Returns: Dict. En exito de la ejecucion HTTP: ``{"status": "ok", "status_code": int, "duration_ms": int, "response_body": str (truncado a 5000 chars), "response_headers": dict, "recorded": bool, "history_id": str|None}``. Si la ejecucion fue ok pero el registro de History fallo, `status` sigue "ok", `recorded` False y se anade `history_error`. Si la ejecucion HTTP falla (RequestException): ``{"status": "error", "error": str, "recorded": False}``. """ variables = variables or {} headers = headers or {} # 1) Resolver placeholders para EJECUTAR (copia; los originales se conservan # para registrarlos sin resolver en el History). resolved_url = _resolve_placeholders(url, variables) resolved_headers = { key: _resolve_placeholders(str(value), variables) for key, value in headers.items() } resolved_body = ( _resolve_placeholders(body, variables) if body is not None else None ) # 2) Ejecutar la peticion real. try: resp = requests.request( method, resolved_url, headers=resolved_headers, data=resolved_body if resolved_body is not None else None, timeout=timeout_s, verify=verify_tls, ) except requests.RequestException as exc: return { "status": "error", "error": f"transport error: {exc}", "recorded": False, } duration_ms = int(resp.elapsed.total_seconds() * 1000) status_code = resp.status_code response_body = resp.text[:_BODY_TRUNCATE] response_headers = dict(resp.headers) result = { "status": "ok", "status_code": status_code, "duration_ms": duration_ms, "response_body": response_body, "response_headers": response_headers, "recorded": False, "history_id": None, } # 3) Registrar en el UserHistory (request SIN resolver, como en la GUI). if not record_history or not access_token: return result spec = { "method": method, "url": url, "headers": headers, "body": body, "body_type": body_type, } req_names = [title] if title else None req_item = build_hoppscotch_collection([spec], request_names=req_names)[ "requests" ][0] req_data = json.dumps(req_item) res_metadata = json.dumps( {"statusCode": status_code, "duration": duration_ms} ) payload = { "query": _HISTORY_MUTATION, "variables": {"d": req_data, "m": res_metadata, "t": "REST"}, } try: hist_resp = requests.post( f"{backend_url}/graphql", json=payload, cookies={"access_token": access_token}, timeout=timeout_s, ) except requests.RequestException as exc: result["history_error"] = f"transport error: {exc}" return result try: hist_data = hist_resp.json() except ValueError: result["history_error"] = ( f"non-JSON history response (HTTP {hist_resp.status_code})" ) return result if hist_data.get("errors"): result["history_error"] = f"graphql errors: {hist_data['errors']}" return result created = (hist_data.get("data") or {}).get("createUserHistory") if not created or not created.get("id"): result["history_error"] = "createUserHistory returned no id" return result result["recorded"] = True result["history_id"] = created["id"] return result