fill aircraft type/registration and terminal/gate from AeroDataBox
This commit is contained in:
+68
-26
@@ -43,6 +43,7 @@ wheels-up/touchdown).
|
||||
import json
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import date, datetime
|
||||
|
||||
@@ -90,6 +91,64 @@ def _utc_offset(local: str | None) -> str | None:
|
||||
return f"{offset[:3]}:{offset[3:]}" if len(offset) == 5 else None
|
||||
|
||||
|
||||
def _get(url: str):
|
||||
"""GET an AeroDataBox endpoint and return the decoded JSON, or None for
|
||||
a "no content" answer. Raises AeroDataBoxError for anything retryable.
|
||||
|
||||
A default urllib request (no User-Agent) gets HTTP 403 from AeroDataBox's
|
||||
Cloudflare-fronted gateway even with a perfectly valid, subscribed key -
|
||||
confirmed live, side by side, same key/URL: bare urllib -> 403, same
|
||||
request + this UA -> 200 with real data. See config.BROWSER_USER_AGENT."""
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"x-rapidapi-key": config.AERODATABOX_API_KEY,
|
||||
"User-Agent": config.BROWSER_USER_AGENT,
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
if resp.status == 204:
|
||||
return None
|
||||
raw = resp.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 204:
|
||||
return None
|
||||
raise AeroDataBoxError(f"HTTP {e.code} from AeroDataBox") from e
|
||||
except urllib.error.URLError as e:
|
||||
raise AeroDataBoxError(f"error reaching AeroDataBox: {e}") from e
|
||||
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
raise AeroDataBoxError(f"bad AeroDataBox response: {e}") from e
|
||||
|
||||
|
||||
def lookup_aircraft_icao(registration: str) -> str | None:
|
||||
"""Registration (e.g. 'VH-8VE') -> ICAO type code (e.g. 'B38M').
|
||||
|
||||
Costs one extra API unit per flight. Needed because AirTrail's
|
||||
`aircraft` field is matched against the `icao` column of its own
|
||||
aircraft table (getAircraftByIcao, confirmed in its source), so the
|
||||
plain model string AeroDataBox puts on the flight record
|
||||
('Boeing 737 MAX 8') is not something AirTrail can resolve."""
|
||||
if not config.AERODATABOX_API_KEY or not registration:
|
||||
return None
|
||||
|
||||
data = _get(f"{BASE_URL}/aircrafts/reg/{urllib.parse.quote(registration)}")
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
# AirTrail tries model before icaoCode against the same column, so both
|
||||
# are worth passing on in that order.
|
||||
for key in ("icaoCode", "model"):
|
||||
value = (data.get(key) or "").strip()
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _first_offset(leg: dict) -> str | None:
|
||||
"""The UTC offset for one leg, from whichever of its timestamps exists.
|
||||
All of a leg's times share the same airport and day, so any of them
|
||||
@@ -126,35 +185,10 @@ def lookup_gate_times(flight_number: str, flight_date: date) -> dict | None:
|
||||
return None
|
||||
|
||||
cleaned = _sanitize(flight_number)
|
||||
url = (
|
||||
data = _get(
|
||||
f"{BASE_URL}/flights/number/{cleaned}/{flight_date.isoformat()}"
|
||||
"?dateLocalRole=Both&withAircraftImage=false&withLocation=false"
|
||||
)
|
||||
# A default urllib request (no User-Agent) gets HTTP 403 from AeroDataBox's
|
||||
# Cloudflare-fronted gateway even with a perfectly valid, subscribed key -
|
||||
# confirmed live, side by side, same key/URL: bare urllib -> 403, same
|
||||
# request + this UA -> 200 with real data. See config.BROWSER_USER_AGENT.
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"x-rapidapi-key": config.AERODATABOX_API_KEY,
|
||||
"User-Agent": config.BROWSER_USER_AGENT,
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
raw = resp.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 204:
|
||||
return None
|
||||
raise AeroDataBoxError(f"HTTP {e.code} from AeroDataBox") from e
|
||||
except urllib.error.URLError as e:
|
||||
raise AeroDataBoxError(f"error reaching AeroDataBox: {e}") from e
|
||||
|
||||
try:
|
||||
data = json.loads(raw.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
raise AeroDataBoxError(f"bad AeroDataBox response: {e}") from e
|
||||
|
||||
if not isinstance(data, list) or not data:
|
||||
return None
|
||||
@@ -191,4 +225,12 @@ def lookup_gate_times(flight_number: str, flight_date: date) -> dict | None:
|
||||
# into the same airport-local terms as everything else here.
|
||||
"departureUtcOffset": _first_offset(dep),
|
||||
"arrivalUtcOffset": _first_offset(arr),
|
||||
# Straight off the flight record, no extra call. aircraftReg is a
|
||||
# free-text field in AirTrail; the ICAO *type* code needs a separate
|
||||
# registration lookup (see lookup_aircraft_icao).
|
||||
"aircraftReg": ((flight.get("aircraft") or {}).get("reg") or "").strip() or None,
|
||||
"departureTerminal": (dep.get("terminal") or "").strip() or None,
|
||||
"departureGate": (dep.get("gate") or "").strip() or None,
|
||||
"arrivalTerminal": (arr.get("terminal") or "").strip() or None,
|
||||
"arrivalGate": (arr.get("gate") or "").strip() or None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user