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)
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'
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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
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.
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.
62@lru_cache(maxsize=128) 63def search( 64 text: str, 65 limit: int = 10, 66 *, 67 is_airport: bool = False, 68 sends_reports: bool = True, 69) -> list[Station]: 70 """Text search for stations against codes, name, city, and state. 71 72 Results may be shorter than limit value. 73 """ 74 try: 75 results = process.extract( 76 text, 77 _CORPUS.value, 78 limit=limit * 20, 79 scorer=fuzz.token_set_ratio, 80 processor=utils.default_process, 81 ) 82 except NameError as name_error: 83 extra = "fuzz" 84 raise MissingExtraModule(extra) from name_error 85 results = [(Station.from_code(k[:4]), s) for k, s, _ in results] 86 results.sort(key=_sort_key, reverse=True) 87 results = [s for s, _ in results if station_filter(s, is_airport=is_airport, reporting=sends_reports)] 88 return results[:limit] if len(results) > limit else results
Text search for stations against codes, name, city, and state.
Results may be shorter than limit value.
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
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.