Implement story 2.1: aircraft data model and fetcher

Add HttpFetcher and FileFixtureFetcher with shared _parse_aircraft helper,
DUMP1090_URL constant, realistic fixture data, and full test coverage for
all acceptance criteria (AC1–AC5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 23:03:20 -04:00
parent 7d89166880
commit 6208134a1c
5 changed files with 236 additions and 35 deletions
+2
View File
@@ -1,5 +1,7 @@
from pathlib import Path
DUMP1090_URL = "http://localhost:8080/data/aircraft.json"
DISPLAY_WIDTH = 800
DISPLAY_HEIGHT = 480
+39
View File
@@ -1,7 +1,46 @@
import json
from pathlib import Path
from typing import Protocol
import requests
from planemapper.constants import DUMP1090_URL, FETCH_TIMEOUT_S
from planemapper.models import Aircraft
class FetcherInterface(Protocol):
def fetch(self) -> list[Aircraft]: ...
def _parse_aircraft(entry: dict) -> Aircraft:
raw_alt = entry.get("altitude", 0)
altitude_ft = int(raw_alt) if isinstance(raw_alt, int) else 0
return Aircraft(
icao=entry["hex"],
lat=float(entry["lat"]),
lon=float(entry["lon"]),
heading=float(entry.get("track", 0.0)),
altitude_ft=altitude_ft,
callsign=entry.get("flight", "").strip(),
category=entry.get("category", ""),
is_mlat=bool(entry.get("mlat")),
is_stale=False,
)
class HttpFetcher:
def fetch(self) -> list[Aircraft]:
resp = requests.get(DUMP1090_URL, timeout=FETCH_TIMEOUT_S)
resp.raise_for_status()
data = resp.json()
return [_parse_aircraft(e) for e in data.get("aircraft", []) if "lat" in e and "lon" in e]
class FileFixtureFetcher:
def __init__(self, path: Path) -> None:
self._path = path
def fetch(self) -> list[Aircraft]:
with self._path.open() as f:
data = json.load(f)
return [_parse_aircraft(e) for e in data.get("aircraft", []) if "lat" in e and "lon" in e]