|
5 | 5 | import httpx |
6 | 6 | from ratelimit import limits, sleep_and_retry |
7 | 7 |
|
8 | | -from opensky.models import FlightLeg, FlightResult |
| 8 | +from opensky.models import FlightLeg, FlightResult, RoundTripResult |
9 | 9 | from opensky.providers import parse_iso_duration |
10 | 10 |
|
11 | 11 | log = logging.getLogger(__name__) |
|
20 | 20 | } |
21 | 21 |
|
22 | 22 |
|
| 23 | +def _convert_offer_slice(offer: dict, slice_index: int, currency: str) -> FlightResult: |
| 24 | + """Extract a single slice from a Duffel offer as a FlightResult (price=0, use offer total).""" |
| 25 | + offer_currency = offer.get("total_currency", currency) |
| 26 | + slices = offer.get("slices", []) |
| 27 | + if slice_index >= len(slices): |
| 28 | + return FlightResult(price=0, currency=offer_currency, duration_minutes=0, stops=0, legs=[], provider="duffel") |
| 29 | + |
| 30 | + slc = slices[slice_index] |
| 31 | + legs: list[FlightLeg] = [] |
| 32 | + total_duration = 0 |
| 33 | + |
| 34 | + for seg in slc.get("segments", []): |
| 35 | + dep = seg.get("departing_at", "") |
| 36 | + arr = seg.get("arriving_at", "") |
| 37 | + dur = parse_iso_duration(seg.get("duration", "")) |
| 38 | + total_duration += dur |
| 39 | + |
| 40 | + carrier = seg.get("operating_carrier", {}) |
| 41 | + airline = carrier.get("iata_code", seg.get("marketing_carrier", {}).get("iata_code", "")) |
| 42 | + flight_num = seg.get("operating_carrier_flight_number") or seg.get("marketing_carrier_flight_number") or "" |
| 43 | + |
| 44 | + legs.append(FlightLeg( |
| 45 | + airline=airline, |
| 46 | + flight_number=flight_num, |
| 47 | + departure_airport=seg.get("origin", {}).get("iata_code", ""), |
| 48 | + arrival_airport=seg.get("destination", {}).get("iata_code", ""), |
| 49 | + departure_time=dep, |
| 50 | + arrival_time=arr, |
| 51 | + duration_minutes=dur, |
| 52 | + )) |
| 53 | + |
| 54 | + return FlightResult( |
| 55 | + price=0, |
| 56 | + currency=offer_currency, |
| 57 | + duration_minutes=total_duration, |
| 58 | + stops=max(len(legs) - 1, 0), |
| 59 | + legs=legs, |
| 60 | + provider="duffel", |
| 61 | + ) |
| 62 | + |
| 63 | + |
23 | 64 | def _convert_offer(offer: dict, currency: str) -> FlightResult: |
24 | 65 | """Convert a Duffel offer dict to a FlightResult.""" |
25 | 66 | price = float(offer.get("total_amount", 0)) |
@@ -118,5 +159,55 @@ def search( |
118 | 159 |
|
119 | 160 | return results |
120 | 161 |
|
| 162 | + @sleep_and_retry |
| 163 | + @limits(calls=10, period=1) |
| 164 | + def search_round_trip( |
| 165 | + self, |
| 166 | + origin: str, |
| 167 | + dest: str, |
| 168 | + outbound_date: str, |
| 169 | + return_date: str, |
| 170 | + cabin: str, |
| 171 | + currency: str, |
| 172 | + max_stops: int | None, |
| 173 | + ) -> list[RoundTripResult]: |
| 174 | + cabin_class = CABIN_MAP.get(cabin, "economy") |
| 175 | + |
| 176 | + payload = { |
| 177 | + "data": { |
| 178 | + "slices": [ |
| 179 | + {"origin": origin, "destination": dest, "departure_date": outbound_date}, |
| 180 | + {"origin": dest, "destination": origin, "departure_date": return_date}, |
| 181 | + ], |
| 182 | + "passengers": [{"type": "adult"}], |
| 183 | + "cabin_class": cabin_class, |
| 184 | + "currency": currency, |
| 185 | + "return_offers": True, |
| 186 | + "max_connections": max_stops if max_stops is not None else 2, |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + resp = self._client.post("/air/offer_requests", json=payload) |
| 191 | + resp.raise_for_status() |
| 192 | + data = resp.json().get("data", {}) |
| 193 | + offers = data.get("offers", []) |
| 194 | + |
| 195 | + results: list[RoundTripResult] = [] |
| 196 | + for offer in offers: |
| 197 | + total_price = float(offer.get("total_amount", 0)) |
| 198 | + offer_currency = offer.get("total_currency", currency) |
| 199 | + outbound = _convert_offer_slice(offer, 0, offer_currency) |
| 200 | + inbound = _convert_offer_slice(offer, 1, offer_currency) |
| 201 | + if max_stops is not None and (outbound.stops > max_stops or inbound.stops > max_stops): |
| 202 | + continue |
| 203 | + results.append(RoundTripResult( |
| 204 | + outbound=outbound, |
| 205 | + inbound=inbound, |
| 206 | + total_price=total_price, |
| 207 | + currency=offer_currency, |
| 208 | + )) |
| 209 | + |
| 210 | + return results |
| 211 | + |
121 | 212 | def close(self) -> None: |
122 | 213 | self._client.close() |
0 commit comments