"""Convierte call specs del registry en una coleccion Hoppscotch importable. Mitad "exportar al GUI" del puente entre el motor de replay del registry y Hoppscotch. La funcion inversa es parse_hoppscotch_collection. """ from urllib.parse import urlparse def _request_name(call: dict, fallback_index: int) -> str: """Deriva un nombre legible para la request a partir del metodo y el path. Args: call: call spec con (opcional) method y url. fallback_index: indice de la call dentro de la lista (no usado en el nombre derivado, reservado para desambiguar si hiciera falta). Returns: nombre del estilo "GET /api/search". """ method = str(call.get("method") or "GET").upper() url = str(call.get("url") or "") path = urlparse(url).path or "/" return f"{method} {path}" def _build_headers(call: dict) -> list[dict]: """Construye la lista de headers Hoppscotch desde el dict del call spec. Convierte el dict headers (preservando orden de insercion) a la lista [{"key", "value", "active": True}, ...] y, si el call spec trae cookies no vacias, anade un header extra "Cookie" al final con formato "k1=v1; k2=v2". Args: call: call spec con (opcional) headers y cookies. Returns: lista de headers Hoppscotch. """ headers: list[dict] = [] raw_headers = call.get("headers") or {} for key, value in raw_headers.items(): headers.append({"key": key, "value": value, "active": True}) cookies = call.get("cookies") or {} if cookies: cookie_value = "; ".join(f"{name}={val}" for name, val in cookies.items()) headers.append({"key": "Cookie", "value": cookie_value, "active": True}) return headers def _build_body(call: dict) -> dict: """Construye el objeto body Hoppscotch segun body_type del call spec. Args: call: call spec con (opcional) body y body_type. Returns: dict con contentType y body. Si no hay body o el body_type es desconocido/None, ambos campos son None. """ body = call.get("body") body_type = call.get("body_type") content_types = { "json": "application/json", "form": "application/x-www-form-urlencoded", "raw": "text/plain", } if body is None or body_type not in content_types: return {"contentType": None, "body": None} return {"contentType": content_types[body_type], "body": body} def build_hoppscotch_collection( calls: list[dict], *, name: str = "Collection", request_names: list[str] | None = None, ) -> dict: """Convierte una lista de call specs en una coleccion Hoppscotch importable. Genera el formato canonico estable (coleccion v:1, request v:"2") que Hoppscotch migra a la ultima version al importar. Pura: sin I/O ni red, solo stdlib, determinista. Args: calls: lista de call specs (salida de har_extract_calls). Cada elemento es un dict con claves opcionales: method, url, headers, cookies, body, body_type. Otras claves (status, sets_cookies, ...) se ignoran. name: nombre de la coleccion Hoppscotch. request_names: nombres explicitos por request, alineados por indice. Si se pasa y existe el indice, sobreescribe el nombre derivado. None = derivar todos los nombres como " ". Returns: dict con la coleccion Hoppscotch: {"v": 1, "name", "folders": [], "requests": [...]}. JSON-serializable. """ requests: list[dict] = [] for index, call in enumerate(calls): if request_names is not None and index < len(request_names): req_name = request_names[index] else: req_name = _request_name(call, index) endpoint = str(call.get("url") or "") method = str(call.get("method") or "GET").upper() requests.append( { "v": "2", "endpoint": endpoint, "name": req_name, "params": [], "headers": _build_headers(call), "method": method, "auth": {"authType": "none", "authActive": True}, "preRequestScript": "", "testScript": "", "body": _build_body(call), "requestVariables": [], } ) return { "v": 1, "name": name, "folders": [], "requests": requests, }