avwx.station

This module contains station/airport dataclasses and search functions.

For the purposes of AVWX, a station is any physical location that has an ICAO or GPS identification code. These are usually airports, but smaller locations might not generate certain report types or defer to larger stations nearby. For example, small airports with an AWOS system might not send the report to NOAA or other local authority. They also include remote weather observation stations not associated with airports like weather buouys.

Classes

 1"""This module contains station/airport dataclasses and search functions.
 2
 3For the purposes of AVWX, a station is any physical location that has an ICAO
 4or GPS identification code. These are usually airports, but smaller locations
 5might not generate certain report types or defer to larger stations nearby. For
 6example, small airports with an AWOS system might not send the report to NOAA
 7or other local authority. They also include remote weather observation stations
 8not associated with airports like weather buouys.
 9
10# Classes
11
12- [avwx.Station](./station/station.html#Station)
13"""
14
15from avwx.station.meta import __LAST_UPDATED__, station_list, uses_na_format, valid_station
16from avwx.station.search import search
17from avwx.station.station import Station, nearest
18
19__all__ = (
20    "Station",
21    "station_list",
22    "nearest",
23    "search",
24    "uses_na_format",
25    "valid_station",
26    "__LAST_UPDATED__",
27)
@dataclass
class Station:
 55@dataclass
 56class Station:
 57    """
 58    The Station dataclass stores basic info about the desired station and
 59    available Runways.
 60
 61    The easiest way to get a station is to supply the ICAO, IATA, or GPS code.
 62    The example below uses `from_code` which checks against all three types,
 63    but you can also use `from_icao`, `from_iata`, or `from_gps` if you know
 64    what type of code you are using. This can be important if you may be using
 65    a code used by more than one station depending on the context. ICAO and
 66    IATA codes are guarenteed unique, but not all airports have them. That
 67    said, all stations available in AVWX have either an ICAO or GPS code.
 68
 69    ```python
 70    >>> from avwx import Station
 71    >>> klex = Station.from_code("KLEX")
 72    >>> f"{klex.name} in {klex.city}, {klex.state}"
 73    'Blue Grass Airport in Lexington, KY'
 74    >>> coord = round(klex.latitude, 3), round(klex.longitude, 3)
 75    >>> f"Located at {coord} at {klex.elevation_ft} feet ({klex.elevation_m} meters)"
 76    'Located at (38.036, -84.606) at 979 feet (298 meters)'
 77    >>> rw = max(klex.runways, key=lambda r: r.length_ft)
 78    >>> f"Its longest runway is {rw.ident1}/{rw.ident2} at {rw.length_ft} feet"
 79    'Its longest runway is 04/22 at 7003 feet'
 80    ```
 81
 82    This is also the same information you'd get from calling Report.station.
 83
 84    ```python
 85    >>> from avwx import Metar
 86    >>> klex = Metar('KLEX')
 87    >>> klex.station.name
 88    'Blue Grass Airport'
 89    ```
 90    """
 91
 92    city: str | None
 93    country: str
 94    elevation_ft: int | None
 95    elevation_m: int | None
 96    gps: str | None
 97    iata: str | None
 98    icao: str | None
 99    latitude: float
100    local: str | None
101    longitude: float
102    name: str
103    note: str | None
104    reporting: bool
105    runways: list[Runway]
106    state: str | None
107    type: str
108    website: str | None
109    wiki: str | None
110
111    @classmethod
112    def _from_code(cls, ident: str) -> Self:
113        try:
114            info: dict[str, Any] = copy(STATIONS[ident])
115            if info["runways"]:
116                info["runways"] = [Runway(**r) for r in info["runways"]]
117            return cls(**info)
118        except (KeyError, AttributeError) as not_found:
119            msg = f"Could not find station with ident {ident}"
120            raise BadStation(msg) from not_found
121
122    @classmethod
123    def from_code(cls, ident: str) -> Self:
124        """Load a Station from ICAO, GPS, or IATA code in that order."""
125        if ident and isinstance(ident, str):
126            if len(ident) == 4:
127                with suppress(BadStation):
128                    return cls.from_icao(ident)
129                with suppress(BadStation):
130                    return cls.from_gps(ident)
131            if len(ident) == 3:
132                with suppress(BadStation):
133                    return cls.from_iata(ident)
134            with suppress(BadStation):
135                return cls.from_local(ident)
136        msg = f"Could not find station with ident {ident}"
137        raise BadStation(msg)
138
139    @classmethod
140    def from_icao(cls, ident: str) -> Self:
141        """Load a Station from an ICAO station ident."""
142        try:
143            return cls._from_code(_ICAO.value[ident.upper()])
144        except (KeyError, AttributeError) as not_found:
145            msg = f"Could not find station with ICAO ident {ident}"
146            raise BadStation(msg) from not_found
147
148    @classmethod
149    def from_iata(cls, ident: str) -> Self:
150        """Load a Station from an IATA code."""
151        try:
152            return cls._from_code(_IATA.value[ident.upper()])
153        except (KeyError, AttributeError) as not_found:
154            msg = f"Could not find station with IATA ident {ident}"
155            raise BadStation(msg) from not_found
156
157    @classmethod
158    def from_gps(cls, ident: str) -> Self:
159        """Load a Station from a GPS code."""
160        try:
161            return cls._from_code(_GPS.value[ident.upper()])
162        except (KeyError, AttributeError) as not_found:
163            msg = f"Could not find station with GPS ident {ident}"
164            raise BadStation(msg) from not_found
165
166    @classmethod
167    def from_local(cls, ident: str) -> Self:
168        """Load a Station from a local code."""
169        try:
170            return cls._from_code(_LOCAL.value[ident.upper()])
171        except (KeyError, AttributeError) as not_found:
172            msg = f"Could not find station with local ident {ident}"
173            raise BadStation(msg) from not_found
174
175    @classmethod
176    def nearest(
177        cls,
178        lat: float | None = None,
179        lon: float | None = None,
180        *,
181        is_airport: bool = False,
182        sends_reports: bool = True,
183        max_coord_distance: float = 10,
184    ) -> tuple[Self, dict] | None:
185        """Load the Station nearest to your location or a lat,lon coordinate pair.
186
187        Returns the Station and distances from source.
188
189        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
190        """
191        if not (lat and lon):
192            lat, lon = _get_ip_location().pair
193        ret = nearest(
194            lat, lon, 1, is_airport=is_airport, sends_reports=sends_reports, max_coord_distance=max_coord_distance
195        )
196        if not isinstance(ret, dict):
197            return None
198        station = ret.pop("station")
199        return station, ret
200
201    @property
202    def lookup_code(self) -> str:
203        """The ICAO or GPS code for report fetch."""
204        if self.icao:
205            return self.icao
206        if self.gps:
207            return self.gps
208        msg = "Station does not have a valid lookup code"
209        raise BadStation(msg)
210
211    @property
212    def storage_code(self) -> str:
213        """The first unique-ish code from what's available."""
214        if self.icao:
215            return self.icao
216        if self.iata:
217            return self.iata
218        if self.gps:
219            return self.gps
220        if self.local:
221            return self.local
222        msg = "Station does not have any useable codes"
223        raise BadStation(msg)
224
225    @property
226    def sends_reports(self) -> bool:
227        """Whether or not a Station likely sends weather reports."""
228        return self.reporting is True
229
230    @property
231    def coord(self) -> Coord:
232        """The station location as a Coord."""
233        return Coord(lat=self.latitude, lon=self.longitude, repr=self.icao)
234
235    def distance(self, lat: float, lon: float) -> Distance:
236        """Geopy Distance using the great circle method."""
237        return great_circle((lat, lon), (self.latitude, self.longitude))
238
239    def nearby(
240        self,
241        *,
242        is_airport: bool = False,
243        sends_reports: bool = True,
244        max_coord_distance: float = 10,
245    ) -> list[tuple[Self, dict]]:
246        """Return Stations nearest to current station and their distances.
247
248        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
249        """
250        stations = nearest(
251            self.latitude,
252            self.longitude,
253            11,
254            is_airport=is_airport,
255            sends_reports=sends_reports,
256            max_coord_distance=max_coord_distance,
257        )
258        if isinstance(stations, dict):
259            return []
260        return [(s.pop("station"), s) for s in stations[1:]]

The Station dataclass stores basic info about the desired station and available Runways.

The easiest way to get a station is to supply the ICAO, IATA, or GPS code. The example below uses from_code which checks against all three types, but you can also use from_icao, from_iata, or from_gps if you know what type of code you are using. This can be important if you may be using a code used by more than one station depending on the context. ICAO and IATA codes are guarenteed unique, but not all airports have them. That said, all stations available in AVWX have either an ICAO or GPS code.

>>> from avwx import Station
>>> klex = Station.from_code("KLEX")
>>> f"{klex.name} in {klex.city}, {klex.state}"
'Blue Grass Airport in Lexington, KY'
>>> coord = round(klex.latitude, 3), round(klex.longitude, 3)
>>> f"Located at {coord} at {klex.elevation_ft} feet ({klex.elevation_m} meters)"
'Located at (38.036, -84.606) at 979 feet (298 meters)'
>>> rw = max(klex.runways, key=lambda r: r.length_ft)
>>> f"Its longest runway is {rw.ident1}/{rw.ident2} at {rw.length_ft} feet"
'Its longest runway is 04/22 at 7003 feet'

This is also the same information you'd get from calling Report.station.

>>> from avwx import Metar
>>> klex = Metar('KLEX')
>>> klex.station.name
'Blue Grass Airport'
Station( city: str | None, country: str, elevation_ft: int | None, elevation_m: int | None, gps: str | None, iata: str | None, icao: str | None, latitude: float, local: str | None, longitude: float, name: str, note: str | None, reporting: bool, runways: list[avwx.station.station.Runway], state: str | None, type: str, website: str | None, wiki: str | None)
city: str | None
country: str
elevation_ft: int | None
elevation_m: int | None
gps: str | None
iata: str | None
icao: str | None
latitude: float
local: str | None
longitude: float
name: str
note: str | None
reporting: bool
runways: list[avwx.station.station.Runway]
state: str | None
type: str
website: str | None
wiki: str | None
@classmethod
def from_code(cls, ident: str) -> Self:
122    @classmethod
123    def from_code(cls, ident: str) -> Self:
124        """Load a Station from ICAO, GPS, or IATA code in that order."""
125        if ident and isinstance(ident, str):
126            if len(ident) == 4:
127                with suppress(BadStation):
128                    return cls.from_icao(ident)
129                with suppress(BadStation):
130                    return cls.from_gps(ident)
131            if len(ident) == 3:
132                with suppress(BadStation):
133                    return cls.from_iata(ident)
134            with suppress(BadStation):
135                return cls.from_local(ident)
136        msg = f"Could not find station with ident {ident}"
137        raise BadStation(msg)

Load a Station from ICAO, GPS, or IATA code in that order.

@classmethod
def from_icao(cls, ident: str) -> Self:
139    @classmethod
140    def from_icao(cls, ident: str) -> Self:
141        """Load a Station from an ICAO station ident."""
142        try:
143            return cls._from_code(_ICAO.value[ident.upper()])
144        except (KeyError, AttributeError) as not_found:
145            msg = f"Could not find station with ICAO ident {ident}"
146            raise BadStation(msg) from not_found

Load a Station from an ICAO station ident.

@classmethod
def from_iata(cls, ident: str) -> Self:
148    @classmethod
149    def from_iata(cls, ident: str) -> Self:
150        """Load a Station from an IATA code."""
151        try:
152            return cls._from_code(_IATA.value[ident.upper()])
153        except (KeyError, AttributeError) as not_found:
154            msg = f"Could not find station with IATA ident {ident}"
155            raise BadStation(msg) from not_found

Load a Station from an IATA code.

@classmethod
def from_gps(cls, ident: str) -> Self:
157    @classmethod
158    def from_gps(cls, ident: str) -> Self:
159        """Load a Station from a GPS code."""
160        try:
161            return cls._from_code(_GPS.value[ident.upper()])
162        except (KeyError, AttributeError) as not_found:
163            msg = f"Could not find station with GPS ident {ident}"
164            raise BadStation(msg) from not_found

Load a Station from a GPS code.

@classmethod
def from_local(cls, ident: str) -> Self:
166    @classmethod
167    def from_local(cls, ident: str) -> Self:
168        """Load a Station from a local code."""
169        try:
170            return cls._from_code(_LOCAL.value[ident.upper()])
171        except (KeyError, AttributeError) as not_found:
172            msg = f"Could not find station with local ident {ident}"
173            raise BadStation(msg) from not_found

Load a Station from a local code.

@classmethod
def nearest( cls, lat: float | None = None, lon: float | None = None, *, is_airport: bool = False, sends_reports: bool = True, max_coord_distance: float = 10) -> tuple[typing.Self, dict] | None:
175    @classmethod
176    def nearest(
177        cls,
178        lat: float | None = None,
179        lon: float | None = None,
180        *,
181        is_airport: bool = False,
182        sends_reports: bool = True,
183        max_coord_distance: float = 10,
184    ) -> tuple[Self, dict] | None:
185        """Load the Station nearest to your location or a lat,lon coordinate pair.
186
187        Returns the Station and distances from source.
188
189        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
190        """
191        if not (lat and lon):
192            lat, lon = _get_ip_location().pair
193        ret = nearest(
194            lat, lon, 1, is_airport=is_airport, sends_reports=sends_reports, max_coord_distance=max_coord_distance
195        )
196        if not isinstance(ret, dict):
197            return None
198        station = ret.pop("station")
199        return station, ret

Load the Station nearest to your location or a lat,lon coordinate pair.

Returns the Station and distances from source.

NOTE: Becomes less accurate toward poles and doesn't cross +/-180

lookup_code: str
201    @property
202    def lookup_code(self) -> str:
203        """The ICAO or GPS code for report fetch."""
204        if self.icao:
205            return self.icao
206        if self.gps:
207            return self.gps
208        msg = "Station does not have a valid lookup code"
209        raise BadStation(msg)

The ICAO or GPS code for report fetch.

storage_code: str
211    @property
212    def storage_code(self) -> str:
213        """The first unique-ish code from what's available."""
214        if self.icao:
215            return self.icao
216        if self.iata:
217            return self.iata
218        if self.gps:
219            return self.gps
220        if self.local:
221            return self.local
222        msg = "Station does not have any useable codes"
223        raise BadStation(msg)

The first unique-ish code from what's available.

sends_reports: bool
225    @property
226    def sends_reports(self) -> bool:
227        """Whether or not a Station likely sends weather reports."""
228        return self.reporting is True

Whether or not a Station likely sends weather reports.

coord: avwx.structs.Coord
230    @property
231    def coord(self) -> Coord:
232        """The station location as a Coord."""
233        return Coord(lat=self.latitude, lon=self.longitude, repr=self.icao)

The station location as a Coord.

def distance(self, lat: float, lon: float) -> geopy.distance.Distance:
235    def distance(self, lat: float, lon: float) -> Distance:
236        """Geopy Distance using the great circle method."""
237        return great_circle((lat, lon), (self.latitude, self.longitude))

Geopy Distance using the great circle method.

def nearby( self, *, is_airport: bool = False, sends_reports: bool = True, max_coord_distance: float = 10) -> list[tuple[typing.Self, dict]]:
239    def nearby(
240        self,
241        *,
242        is_airport: bool = False,
243        sends_reports: bool = True,
244        max_coord_distance: float = 10,
245    ) -> list[tuple[Self, dict]]:
246        """Return Stations nearest to current station and their distances.
247
248        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
249        """
250        stations = nearest(
251            self.latitude,
252            self.longitude,
253            11,
254            is_airport=is_airport,
255            sends_reports=sends_reports,
256            max_coord_distance=max_coord_distance,
257        )
258        if isinstance(stations, dict):
259            return []
260        return [(s.pop("station"), s) for s in stations[1:]]

Return Stations nearest to current station and their distances.

NOTE: Becomes less accurate toward poles and doesn't cross +/-180

@lru_cache(maxsize=2)
def station_list(*, reporting: bool = True) -> list[str]:
21@lru_cache(maxsize=2)
22def station_list(*, reporting: bool = True) -> list[str]:
23    """Return a list of station idents matching the search criteria."""
24    return [code for code, station in STATIONS.items() if not reporting or station["reporting"]]

Return a list of station idents matching the search criteria.

def nearest( lat: float, lon: float, n: int = 1, *, is_airport: bool = False, sends_reports: bool = True, max_coord_distance: float = 10) -> dict | list[dict]:
335def nearest(
336    lat: float,
337    lon: float,
338    n: int = 1,
339    *,
340    is_airport: bool = False,
341    sends_reports: bool = True,
342    max_coord_distance: float = 10,
343) -> dict | list[dict]:
344    """Find the nearest n Stations to a lat,lon coordinate pair.
345
346    Returns the Station and coordinate distance from source.
347
348    NOTE: Becomes less accurate toward poles and doesn't cross +/-180.
349    """
350    # Default state includes all, no filtering necessary
351    if is_airport or sends_reports:
352        stations = _query_filter(lat, lon, n, max_coord_distance, is_airport=is_airport, reporting=sends_reports)
353    else:
354        data = _query_coords(lat, lon, n, max_coord_distance)
355        stations = [(Station.from_code(code), d) for code, d in data]
356    if not stations:
357        return []
358    ret = []
359    for station, coord_dist in stations:
360        dist = station.distance(lat, lon)
361        ret.append(
362            {
363                "station": station,
364                "coordinate_distance": coord_dist,
365                "nautical_miles": dist.nautical,
366                "miles": dist.miles,
367                "kilometers": dist.kilometers,
368            }
369        )
370    if n == 1:
371        return ret[0]
372    ret.sort(key=lambda x: x["miles"])
373    return ret

Find the nearest n Stations to a lat,lon coordinate pair.

Returns the Station and coordinate distance from source.

NOTE: Becomes less accurate toward poles and doesn't cross +/-180.

def uses_na_format(station: str, default: bool | None = None) -> bool:
27def uses_na_format(station: str, default: bool | None = None) -> bool:
28    """Return True if the station uses the North American format.
29
30    False if the International format
31    """
32    if station[0] in NA_REGIONS:
33        return True
34    if station[0] in IN_REGIONS:
35        return False
36    if station[:2] in M_NA_REGIONS:
37        return True
38    if station[:2] in M_IN_REGIONS:
39        return False
40    if default is not None:
41        return default
42    msg = "Station doesn't start with a recognized character set"
43    raise BadStation(msg)

Return True if the station uses the North American format.

False if the International format

def valid_station(station: str) -> None:
46def valid_station(station: str) -> None:
47    """Check the validity of a station ident.
48
49    This function doesn't return anything. It merely raises a BadStation error if needed.
50    """
51    station = station.strip()
52    if len(station) != 4:
53        msg = "Report station ident must be four characters long"
54        raise BadStation(msg)
55    uses_na_format(station)

Check the validity of a station ident.

This function doesn't return anything. It merely raises a BadStation error if needed.

__LAST_UPDATED__ = '2024-12-02'