Spaces:
Sleeping
Sleeping
File size: 4,496 Bytes
8ae2973 d8c3cfa bc18056 d8c3cfa 8ae2973 d8c3cfa 8ae2973 d8c3cfa 8ae2973 d8c3cfa 8ae2973 d8c3cfa 8ae2973 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | """Popular destinations and cheap flights endpoints."""
from __future__ import annotations
from datetime import date, timedelta
from fastapi import APIRouter, Query
from ..data_loader import get_route_graph
from ..price_engine import compute_calendar_price
from ..seed_utils import seeded_random
router = APIRouter(prefix="/api/destinations", tags=["destinations"])
def _cheapest_in_window(origin: str, dest: str, distance_km: int, num_carriers: int, dest_continent: str) -> tuple[float, str]:
"""Find the cheapest economy price across the next 90 days.
Returns (cheapest_price, cheapest_date_iso).
"""
from ..benchmark import today as _today
today = _today()
best_price = float("inf")
best_date = today + timedelta(days=14)
for day_offset in range(1, 91):
target = today + timedelta(days=day_offset)
rng = seeded_random("indicative", origin, dest, target.isoformat())
price = compute_calendar_price(
distance_km=distance_km,
cabin_class="economy",
target_date=target,
num_carriers=num_carriers,
dest_continent=dest_continent,
rng=rng,
)
if price < best_price:
best_price = price
best_date = target
return best_price, best_date.isoformat()
@router.get("/popular")
async def popular_destinations(
origin: str = Query(..., min_length=3, max_length=3),
limit: int = Query(8, ge=1, le=20),
):
"""Return top destinations from an origin, ranked by destination hub_score."""
graph = get_route_graph()
if origin.upper() not in graph.airports:
return {"destinations": []}
origin_upper = origin.upper()
routes = graph.get_outbound_routes(origin_upper)
origin_airport = graph.airports[origin_upper]
scored = []
for dest_iata, route in routes.items():
dest_airport = graph.airports.get(dest_iata)
if not dest_airport:
continue
price, cheapest_date = _cheapest_in_window(
origin_upper, dest_iata, route.distance_km,
len(route.carriers), dest_airport.continent,
)
scored.append({
"iata": dest_iata,
"city": dest_airport.city_name,
"country": dest_airport.country,
"country_code": dest_airport.country_code,
"price_usd": round(price),
"cheapest_date": cheapest_date,
"hub_score": dest_airport.hub_score,
"distance_km": route.distance_km,
"is_domestic": dest_airport.country_code == origin_airport.country_code,
})
# Sort by hub_score descending (most popular destinations first)
scored.sort(key=lambda x: -x["hub_score"])
return {"origin": origin_upper, "destinations": scored[:limit]}
@router.get("/cheap-flights")
async def cheap_flights(
origin: str = Query(..., min_length=3, max_length=3),
category: str = Query("popular", pattern="^(domestic|international|popular)$"),
limit: int = Query(8, ge=1, le=20),
):
"""Return cheapest flights from an origin, filtered by category."""
graph = get_route_graph()
if origin.upper() not in graph.airports:
return {"flights": []}
origin_upper = origin.upper()
routes = graph.get_outbound_routes(origin_upper)
origin_airport = graph.airports[origin_upper]
items = []
for dest_iata, route in routes.items():
dest_airport = graph.airports.get(dest_iata)
if not dest_airport:
continue
is_domestic = dest_airport.country_code == origin_airport.country_code
if category == "domestic" and not is_domestic:
continue
if category == "international" and is_domestic:
continue
price, cheapest_date = _cheapest_in_window(
origin_upper, dest_iata, route.distance_km,
len(route.carriers), dest_airport.continent,
)
items.append({
"iata": dest_iata,
"city": dest_airport.city_name,
"country": dest_airport.country,
"country_code": dest_airport.country_code,
"price_usd": round(price),
"cheapest_date": cheapest_date,
"distance_km": route.distance_km,
"is_domestic": is_domestic,
})
# Sort by price ascending (cheapest first)
items.sort(key=lambda x: x["price_usd"])
return {"origin": origin_upper, "category": category, "flights": items[:limit]}
|