import os import json from datetime import date from typing import Optional from pydantic import BaseModel from serpapi import GoogleSearch import asyncio BRAZIL_AIRPORTS = { # São Paulo "são paulo": "GRU,CGH", "sao paulo": "GRU,CGH", "saopaulo": "GRU,CGH", "sãopaulo": "GRU,CGH", "sp": "GRU,CGH", "guarulhos": "GRU", "campinas": "VCP", "viracopos": "VCP", "congonhas": "CGH", # Sudeste "rio de janeiro": "GIG,SDU", "riodejaneiro": "GIG,SDU", "rj": "GIG,SDU", "belo horizonte": "CNF,PLU", "belohorizonte": "CNF,PLU", "bh": "CNF,PLU", "vitória": "VIX", "vitoria": "VIX", "juiz de fora": "IZA", "juizdefora": "IZA", "uberlândia": "UDI", "uberlandia": "UDI", "montes claros": "MOC", "montesclaros": "MOC", "governador valadares": "GVR", "governadorvaladares": "GVR", "uberaba": "UBA", # Nordeste "salvador": "SSA", "bahia": "SSA", "recife": "REC", "fortaleza": "FOR", "natal": "NAT", "maceió": "MCZ", "maceio": "MCZ", "joão pessoa": "JPA", "joao pessoa": "JPA", "joaopessoa": "JPA", "aracaju": "AJU", "são luís": "SLZ", "sao luis": "SLZ", "saoluis": "SLZ", "teresina": "THE", "ilhéus": "IOS", "ilheus": "IOS", "porto seguro": "BPS", "portoseguro": "BPS", "vitoria da conquista": "VDC", "vitória da conquista": "VDC", "vitóriadaconquista": "VDC", "feira de santana": "FEC", "feiraadesantana": "FEC", "petrolina": "PNZ", "paulo afonso": "PAV", "pauloafonso": "PAV", "juazeiro do norte": "JDO", "juazeirodonorte": "JDO", "campina grande": "CPV", "campinavgrande": "CPV", "mossoró": "MVF", "mossoro": "MVF", "imperatriz": "IMP", "barreiras": "BRA", # Sul "porto alegre": "POA", "portoalegre": "POA", "curitiba": "CWB", "florianópolis": "FLN", "florianopolis": "FLN", "navegantes": "NVT", "joinville": "JOI", "londrina": "LDB", "foz do iguaçu": "IGU", "fozdoiguacu": "IGU", "maringá": "MGF", "maringa": "MGF", "chapecó": "XAP", "chapeco": "XAP", "passo fundo": "PFB", "passofundo": "PFB", "caxias do sul": "CXJ", "caxiasdosul": "CXJ", "cascavel": "CAC", # Centro-Oeste "brasília": "BSB", "brasilia": "BSB", "goiânia": "GYN", "goiania": "GYN", "cuiabá": "CGB", "cuiaba": "CGB", "campo grande": "CGR", "campogrande": "CGR", "corumbá": "CMG", "corumba": "CMG", "rondonópolis": "ROO", "rondonopolis": "ROO", "sinop": "OPS", # Norte "manaus": "MAO", "belém": "BEL", "belem": "BEL", "porto velho": "PVH", "portovelho": "PVH", "boa vista": "BVB", "boavista": "BVB", "palmas": "PMW", "macapá": "MCP", "macapa": "MCP", "rio branco": "RBR", "riobranco": "RBR", "santarém": "STM", "santarem": "STM", "marabá": "MAB", "maraba": "MAB", "altamira": "ATM", "carajás": "CKS", "carajas": "CKS", } def city_to_iata(city: str) -> str: normalized = city.lower().strip() # 1. Match exato primeiro if normalized in BRAZIL_AIRPORTS: return BRAZIL_AIRPORTS[normalized] # 2. Busca parcial — pega a chave mais longa (mais específica) best_key = None best_iata = None for key, iata in BRAZIL_AIRPORTS.items(): if key in normalized or normalized in key: if best_key is None or len(key) > len(best_key): best_key = key best_iata = iata if best_iata: return best_iata return city.upper()[:3] class FlightQuery(BaseModel): origin_city: str destination_city: str departure_date: date return_date: Optional[date] = None adults: int = 1 def _search_flights_sync(query: FlightQuery) -> str: origin = city_to_iata(query.origin_city) dest = city_to_iata(query.destination_city) params = { "engine": "google_flights", "departure_id": origin, "arrival_id": dest, "outbound_date": query.departure_date.isoformat(), "currency": "BRL", "hl": "pt", "gl": "br", "adults": query.adults, "sort_by": "2", # preço "api_key": os.getenv("SERPAPI_KEY"), } # ida e volta if query.return_date: params["type"] = "1" params["return_date"] = query.return_date.isoformat() else: params["type"] = "2" results = GoogleSearch(params).get_dict() all_flights = results.get("best_flights", []) + results.get("other_flights", []) if not all_flights: return json.dumps({"error": "Nenhum voo encontrado para essa rota/data."}) flights_out = [] for f in all_flights[:8]: legs = f["flights"] first, last = legs[0], legs[-1] h, m = divmod(f["total_duration"], 60) flights_out.append({ "airline": first["airline"], "origin_airport": first["departure_airport"]["id"], "origin_name": first["departure_airport"].get("name", ""), "dest_airport": last["arrival_airport"]["id"], "dest_name": last["arrival_airport"].get("name", ""), "departure": first["departure_airport"]["time"], "arrival": last["arrival_airport"]["time"], "duration": f"{h}h{m:02d}m", "stops": len(legs) - 1, "stop_cities": [ leg["arrival_airport"]["id"] for leg in legs[:-1] ] if len(legs) > 1 else [], "price_brl": f["price"], }) insights = results.get("price_insights", {}) return json.dumps({ "route": f"{origin} → {dest}", "date": query.departure_date.isoformat(), "total_found": len(all_flights), "flights": flights_out, "price_insights": { "level": insights.get("price_level", ""), "lowest": insights.get("lowest_price"), "typical_range": insights.get("typical_price_range", []), }, }, ensure_ascii=False, indent=2) async def search_flights_serpapi(query: FlightQuery) -> str: """Wrapper assíncrono — roda a chamada bloqueante em thread pool.""" loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _search_flights_sync, query)