fill aircraft type/registration and terminal/gate from AeroDataBox

This commit is contained in:
2026-09-15 22:58:54 +10:00
parent 480b896d39
commit 4ed4dd1ec2
6 changed files with 391 additions and 29 deletions
+68 -26
View File
@@ -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,
}