├── .gitignore ├── AUTHORS ├── HACKING ├── README.rst ├── TODO ├── bin └── noaa ├── noaa ├── __init__.py ├── exceptions.py ├── forecast.py ├── geocode.py ├── models.py ├── observation.py ├── stations.py └── utils.py ├── setup.py └── tools ├── generate_authors.sh ├── pip-requires └── run_pep8.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.DS_Store 4 | build 5 | dist 6 | noaa.egg-info 7 | *.log 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Rick Harris 2 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Steps Before Making a Pull Request 2 | ----------------------------------- 3 | 4 | Squash any commits in topic branch (rebase or merge/squash). 5 | 6 | (Re)generate AUTHORS file:: 7 | 8 | ./tools/generate_authors.sh > AUTHORS 9 | 10 | Run pep8:: 11 | 12 | ./tools/run_pep8.sh 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | python-noaa 3 | =========== 4 | 5 | 6 | Description 7 | =========== 8 | 9 | Python bindings for NOAA's XML web-service as well as a simple command-line 10 | utility for fetching the forecast. 11 | 12 | 13 | Requires 14 | ======== 15 | 16 | * Python 2.6+ 17 | * argparse (if Python < 2.7) 18 | * dateutils 19 | 20 | 21 | Command-Line Usage 22 | ================== 23 | 24 | By Zip Code 25 | ----------- 26 | 27 | :: 28 | 29 | $ noaa 78703 30 | Forecast for 78703 31 | Fri Chance Rain Showers 57 F 70 F +++++++++++++++++++++++++++++++++++ 32 | Sat Rain Showers Likely 60 F 72 F ++++++++++++++++++++++++++++++++++++ 33 | Sun Rain Showers Likely 53 F 54 F +++++++++++++++++++++++++++ 34 | Mon Chance Rain 40 F 45 F ++++++++++++++++++++++ 35 | Tue Mostly Sunny 34 F 47 F +++++++++++++++++++++++ 36 | 37 | By Location 38 | ----------- 39 | 40 | :: 41 | 42 | $ noaa Austin, TX 43 | Forecast for Austin, TX, USA 44 | Fri Chance Rain Showers 57 F 70 F +++++++++++++++++++++++++++++++++++ 45 | Sat Rain Showers Likely 60 F 72 F ++++++++++++++++++++++++++++++++++++ 46 | Sun Rain Showers Likely 53 F 54 F +++++++++++++++++++++++++++ 47 | Mon Chance Rain 40 F 45 F ++++++++++++++++++++++ 48 | Tue Mostly Sunny 34 F 47 F +++++++++++++++++++++++ 49 | 50 | By Coordinates 51 | -------------- 52 | 53 | :: 54 | 55 | $ noaa --metric 30.29128 -97.7385 56 | Forecast for 30.29128, -97.7385 57 | Fri Chance Rain Showers 14 C 21 C +++++++++++++++++++++ 58 | Sat Rain Showers Likely 16 C 22 C ++++++++++++++++++++++ 59 | Sun Rain Showers Likely 12 C 12 C ++++++++++++ 60 | Mon Chance Rain 4 C 7 C +++++++ 61 | Tue Cold 1 C 8 C ++++++++ 62 | 63 | 64 | Example ~/.noaarc 65 | ================= 66 | 67 | :: 68 | 69 | [default] 70 | location=78705 71 | metric=False 72 | heading=False 73 | color=True 74 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Add hourly forecast 2 | -------------------------------------------------------------------------------- /bin/noaa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (c) 2011 Rick Harris 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | 22 | """ 23 | Command-line utility for fetching data from NOAA National Digital Forecast 24 | Database (NDFD). 25 | """ 26 | 27 | import argparse 28 | import ConfigParser 29 | import os 30 | import sys 31 | 32 | import noaa 33 | from noaa import exceptions 34 | from noaa import forecast 35 | from noaa import utils 36 | 37 | 38 | def temp_color(temp_f): 39 | if temp_f >= 90: 40 | return "red" # hot 41 | elif temp_f >= 68: 42 | return "yellow" # warm 43 | elif temp_f >= 55: 44 | return "default" # nice 45 | elif temp_f >= 32: 46 | return "blue" # cold 47 | elif temp_f > 0: 48 | return "cyan" # freezing 49 | else: 50 | return "magenta" # below zero 51 | 52 | 53 | def simple_temp_graph(temp, scale=0.5, color=True): 54 | value = temp.farenheit 55 | char = '+' if value >= 0 else '-' 56 | scaled_value = int(abs(value * scale)) 57 | graph_str = char * scaled_value 58 | 59 | if color: 60 | graph_str = utils.colorize(graph_str, temp_color(temp.farenheit)) 61 | 62 | return graph_str 63 | 64 | 65 | def format_temp(temp, padding=5, color=True): 66 | temp_str = str(temp.value).rjust(padding) 67 | 68 | if color: 69 | temp_str = utils.colorize(temp_str, temp_color(temp.farenheit)) 70 | 71 | return " ".join([temp_str, temp.unit]) 72 | 73 | 74 | def conditions_color(conditions): 75 | if 'Sunny' in conditions: 76 | return 'yellow' 77 | elif 'Rain' in conditions: 78 | return 'cyan' 79 | elif 'Drizzle' in conditions: 80 | return 'green' 81 | elif 'Thunderstorms' in conditions: 82 | return 'red' 83 | elif 'Cold' in conditions: 84 | return 'blue' 85 | elif 'Snow' in conditions: 86 | return 'white' 87 | else: 88 | return "default" 89 | 90 | 91 | def format_conditions(conditions, padding=30, color=True): 92 | conditions_str = conditions.ljust(padding) 93 | 94 | if color: 95 | conditions_str = utils.colorize( 96 | conditions_str, conditions_color(conditions)) 97 | 98 | return conditions_str 99 | 100 | 101 | def config_get_boolean(config, section, option, default=None): 102 | try: 103 | value = config.getboolean(section, option) 104 | except ConfigParser.NoSectionError: 105 | value = default 106 | except ConfigParser.NoOptionError: 107 | value = default 108 | 109 | return value 110 | 111 | 112 | def make_parser(): 113 | config = ConfigParser.ConfigParser() 114 | config.read(os.path.expanduser("~/.noaarc")) 115 | 116 | try: 117 | default_location = config.get('default', 'location').split() 118 | except ConfigParser.NoSectionError: 119 | default_location = None 120 | except ConfigParser.NoOptionError: 121 | default_location = None 122 | 123 | default_metric = config_get_boolean( 124 | config, 'default', 'metric', default=False) 125 | 126 | default_heading = config_get_boolean( 127 | config, 'default', 'heading', default=True) 128 | 129 | default_color = config_get_boolean( 130 | config, 'default', 'color', default=False) 131 | 132 | parser = argparse.ArgumentParser() 133 | parser.add_argument('location', 134 | nargs="*", 135 | default=default_location) 136 | parser.add_argument('--metric', 137 | action="store_true", 138 | default=default_metric, 139 | help="use Celsius for temperatures.") 140 | parser.add_argument('--imperial', 141 | action="store_false", 142 | dest="metric", 143 | help="use Celsius for temperatures.") 144 | parser.add_argument('--heading', 145 | action="store_true", 146 | dest='heading', 147 | default=default_heading, 148 | help="display location heading.") 149 | parser.add_argument('--no-heading', 150 | action="store_false", 151 | dest='heading', 152 | help="don't display location heading.") 153 | parser.add_argument('--color', 154 | action="store_true", 155 | default=default_color, 156 | help="colorize results.") 157 | parser.add_argument('--no-color', 158 | action="store_false", 159 | dest="color", 160 | help="don't colorize results.") 161 | parser.add_argument('--version', action='version', 162 | version='%(prog)s ' + noaa.__version__) 163 | return parser 164 | 165 | 166 | def daily_forecast_by_location(args): 167 | location = " ".join(args.location) 168 | 169 | pretty_location, fcast = forecast.daily_forecast_by_location( 170 | location, metric=args.metric) 171 | 172 | return pretty_location, fcast 173 | 174 | 175 | def daily_forecast_by_zip_code(args): 176 | zip_code = "".join(args.location) 177 | 178 | fcast = forecast.daily_forecast_by_zip_code( 179 | zip_code, metric=args.metric) 180 | 181 | pretty_location = zip_code 182 | return pretty_location, fcast 183 | 184 | 185 | def daily_forecast_by_lat_lon(args): 186 | lat, lon = args.location 187 | 188 | fcast = forecast.daily_forecast_by_lat_lon( 189 | lat, lon, metric=args.metric) 190 | 191 | pretty_location = ', '.join([lat, lon]) 192 | return pretty_location, fcast 193 | 194 | 195 | def args_overrides(args): 196 | """Perform any argument overrides.""" 197 | if not sys.stdout.isatty(): 198 | args.color = False 199 | 200 | 201 | def main(): 202 | parser = make_parser() 203 | args = parser.parse_args() 204 | args_overrides(args) 205 | 206 | if not utils.all_numbers(args.location): 207 | # Args not being all numbers implies we were passed a string location 208 | forecast_func = daily_forecast_by_location 209 | elif len(args.location) == 1: 210 | # All numbers with one argument implies zip code 211 | forecast_func = daily_forecast_by_zip_code 212 | elif len(args.location) == 2: 213 | # 3 args that are all numbers implies lat lon coordinates 214 | forecast_func = daily_forecast_by_lat_lon 215 | else: 216 | parser.print_help() 217 | sys.exit(1) 218 | 219 | with utils.die_on(exceptions.NOAAException): 220 | pretty_location, fcast = forecast_func(args) 221 | 222 | if args.heading: 223 | print "Forecast for %s" % pretty_location 224 | 225 | for datapoint in fcast: 226 | print datapoint.date.strftime('%a'), 227 | print format_conditions(datapoint.conditions, color=args.color), 228 | print format_temp(datapoint.min_temp, color=args.color), 229 | print format_temp(datapoint.max_temp, color=args.color), 230 | print simple_temp_graph(datapoint.max_temp, color=args.color) 231 | 232 | 233 | if __name__ == "__main__": 234 | main() 235 | -------------------------------------------------------------------------------- /noaa/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Rick Harris 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | Python bindings to the NOAA National Digital Forecast Database (NDFD) 23 | """ 24 | 25 | __version__ = "0.2.1" 26 | -------------------------------------------------------------------------------- /noaa/exceptions.py: -------------------------------------------------------------------------------- 1 | class NOAAException(Exception): 2 | pass 3 | 4 | 5 | class GeocodeException(NOAAException): 6 | pass 7 | -------------------------------------------------------------------------------- /noaa/forecast.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from noaa import exceptions 4 | from noaa import geocode 5 | from noaa import models 6 | from noaa import utils 7 | 8 | 9 | def daily_forecast_by_zip_code(zip_code, start_date=None, num_days=6, 10 | metric=False): 11 | """Return a daily forecast by zip code. 12 | 13 | :param zip_code: 14 | :param start_date: 15 | :param num_days: 16 | :returns: [ForecastedCondition() ...] 17 | """ 18 | location_info = [("zipCodeList", zip_code)] 19 | return _daily_forecast_from_location_info( 20 | location_info, start_date=start_date, num_days=num_days, 21 | metric=metric) 22 | 23 | 24 | def daily_forecast_by_lat_lon(lat, lon, start_date=None, num_days=6, 25 | metric=False): 26 | """Return a daily forecast by lat lon. 27 | 28 | :param lat: 29 | :param lon: 30 | :param start_date: 31 | :param num_days: 32 | :returns: [ForecastedCondition() ...] 33 | """ 34 | location_info = [("lat", lat), ("lon", lon)] 35 | return _daily_forecast_from_location_info( 36 | location_info, start_date=start_date, num_days=num_days, 37 | metric=metric) 38 | 39 | 40 | def daily_forecast_by_location(location, start_date=None, num_days=6, 41 | metric=False): 42 | """Return a daily forecast by location. 43 | 44 | :param location: A location string that will be geocoded (ex. "Austin") 45 | :param start_date: 46 | :param num_days: 47 | :returns: [ForecastedCondition() ...] 48 | """ 49 | loc = geocode.geocode_location(location) 50 | return loc.description, daily_forecast_by_lat_lon( 51 | loc.lat, loc.lon, start_date=start_date, num_days=num_days, 52 | metric=metric) 53 | 54 | 55 | def _parse_time_layouts(tree): 56 | """Return a dictionary containing the time-layouts 57 | 58 | A time-layout looks like: 59 | 60 | { 'time-layout-key': [(start-time, end-time), ...] } 61 | """ 62 | parse_dt = utils.parse_dt 63 | time_layouts = {} 64 | for tl_elem in tree.getroot().getiterator(tag="time-layout"): 65 | start_times = [] 66 | end_times = [] 67 | for tl_child in tl_elem.getchildren(): 68 | if tl_child.tag == "layout-key": 69 | key = tl_child.text 70 | elif tl_child.tag == "start-valid-time": 71 | dt = parse_dt(tl_child.text) 72 | start_times.append(dt) 73 | elif tl_child.tag == "end-valid-time": 74 | dt = parse_dt(tl_child.text) 75 | end_times.append(dt) 76 | 77 | time_layouts[key] = zip(start_times, end_times) 78 | 79 | return time_layouts 80 | 81 | 82 | def _parse_temperatures_for_type(tree, temp_type): 83 | for tmp_e in tree.getroot().getiterator(tag='temperature'): 84 | if tmp_e.attrib['type'] != temp_type: 85 | continue 86 | values = [] 87 | for val_e in tmp_e.getiterator(tag='value'): 88 | try: 89 | val = int(val_e.text) 90 | except TypeError: 91 | # Temp can be none if we don't have a forecast for that 92 | # date 93 | val = None 94 | values.append(val) 95 | 96 | time_layout_key = tmp_e.attrib['time-layout'] 97 | return time_layout_key, values 98 | 99 | raise Exception("temp type '%s' not found in data") 100 | 101 | 102 | def _parse_conditions(tree): 103 | for weather_e in tree.getroot().getiterator(tag='weather'): 104 | values = [] 105 | for condition_e in weather_e.getiterator(tag='weather-conditions'): 106 | value = condition_e.attrib.get('weather-summary') 107 | values.append(value) 108 | 109 | time_layout_key = weather_e.attrib['time-layout'] 110 | return time_layout_key, values 111 | 112 | 113 | def _daily_forecast_from_location_info(location_info, start_date=None, 114 | num_days=6, metric=False): 115 | if not start_date: 116 | start_date = datetime.date.today() 117 | 118 | # NOTE: the order of the query-string parameters seems to matter; so, 119 | # we can't use a dictionary to hold the params 120 | params = location_info + [("format", "24 hourly"), 121 | ("startDate", start_date.strftime("%Y-%m-%d")), 122 | ("numDays", str(num_days)), 123 | ("Unit", "m" if metric else "e")] 124 | 125 | FORECAST_BY_DAY_URL = ("http://www.weather.gov/forecasts/xml" 126 | "/sample_products/browser_interface" 127 | "/ndfdBrowserClientByDay.php") 128 | 129 | resp = utils.open_url(FORECAST_BY_DAY_URL, params) 130 | tree = utils.parse_xml(resp) 131 | 132 | if tree.getroot().tag == 'error': 133 | raise exceptions.NOAAException("Unable to retrieve forecast") 134 | 135 | time_layouts = _parse_time_layouts(tree) 136 | min_temp_tlk, min_temps = _parse_temperatures_for_type(tree, 'minimum') 137 | max_temp_tlk, max_temps = _parse_temperatures_for_type(tree, 'maximum') 138 | conditions_tlk, conditions = _parse_conditions(tree) 139 | 140 | # Time layout keys have to match for us to sequence and group by them 141 | assert (min_temp_tlk == max_temp_tlk == conditions_tlk) 142 | 143 | time_layout_key = min_temp_tlk 144 | time_layout = time_layouts[time_layout_key] 145 | dates = [dt.date() for dt, _ in time_layout] 146 | 147 | forecast = [] 148 | for date, min_temp_value, max_temp_value, condition in zip( 149 | dates, min_temps, max_temps, conditions): 150 | 151 | # If we're missing any data, don't create the data point 152 | if utils.any_none([min_temp_value, max_temp_value, condition]): 153 | continue 154 | 155 | temp_unit = 'C' if metric else 'F' 156 | min_temp = models.Temperature(min_temp_value, unit=temp_unit) 157 | max_temp = models.Temperature(max_temp_value, unit=temp_unit) 158 | datapoint = models.ForecastedCondition( 159 | date, min_temp, max_temp, condition) 160 | forecast.append(datapoint) 161 | 162 | return forecast 163 | -------------------------------------------------------------------------------- /noaa/geocode.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from noaa import exceptions 4 | from noaa import models 5 | from noaa import utils 6 | 7 | 8 | def geocode_location(location, api_key=None): 9 | """Use Google to geocode a location string. 10 | 11 | For high-volume traffic, you will need to specify an API-key. 12 | """ 13 | GEOCODE_URL = "http://maps.google.com/maps/geo" 14 | params = [('q', location), 15 | ('sensor', 'false'), 16 | ('output', 'json')] 17 | 18 | if api_key: 19 | params += [('key', api_key)] 20 | 21 | resp = utils.open_url(GEOCODE_URL, params) 22 | data = json.loads(resp.read()) 23 | 24 | if data['Status']['code'] != 200: 25 | raise exceptions.GeocodeException('Unable to geocode this location') 26 | 27 | best_match = data['Placemark'][0] 28 | address = best_match['address'] 29 | lon, lat, _ = best_match['Point']['coordinates'] 30 | 31 | location = models.Location(lat, lon, address) 32 | return location 33 | -------------------------------------------------------------------------------- /noaa/models.py: -------------------------------------------------------------------------------- 1 | class Dimension(object): 2 | def __init__(self, value, unit): 3 | self.value = value 4 | self.unit = unit 5 | 6 | 7 | class Speed(Dimension): 8 | @property 9 | def kph(self): 10 | #TODO: write this 11 | pass 12 | 13 | @property 14 | def mph(self): 15 | #TODO: write this 16 | pass 17 | 18 | 19 | class Vector(object): 20 | def __init__(self, speed, direction): 21 | self.speed = speed 22 | self.direction = direction 23 | 24 | 25 | class Location(object): 26 | def __init__(self, lat, lon, description): 27 | self.lat = lat 28 | self.lon = lon 29 | self.description = description 30 | 31 | 32 | class Pressure(Dimension): 33 | @property 34 | def inches(self): 35 | #TODO: write this 36 | pass 37 | 38 | @property 39 | def millibars(self): 40 | #TODO: write this 41 | pass 42 | 43 | 44 | class Temperature(Dimension): 45 | @property 46 | def farenheit(self): 47 | if self.unit == "F": 48 | return self.value 49 | return (9.0 / 5) * self.value + 32 50 | 51 | @property 52 | def celsius(self): 53 | if self.unit == "C": 54 | return self.value 55 | return (5.0 / 9) * (self.value - 32) 56 | 57 | 58 | class Wind(object): 59 | def __init__(self, vector, intensity): 60 | self.vector = vector 61 | self.intensity = intensity 62 | 63 | 64 | class ForecastedCondition(object): 65 | def __init__(self, date, min_temp, max_temp, conditions): 66 | self.date = date 67 | self.min_temp = min_temp 68 | self.max_temp = max_temp 69 | self.conditions = conditions 70 | 71 | 72 | class Station(object): 73 | def __init__(self, station_id, location): 74 | self.station_id = station_id 75 | self.location = location 76 | 77 | 78 | class Observation(object): 79 | """ 80 | NOAA's stations may return different observation parameters. Station 81 | identification and pickup information appears to be consistent, but 82 | nothing weather related can be assumed to be present. 83 | 84 | If you want the temperture for a location, you must search all stations 85 | nearby for a station that provides the `temp_f` parameter. 86 | """ 87 | def __init__(self, updated_at, temp): 88 | self.updated_at = updated_at 89 | self.temp = temp 90 | 91 | 92 | class StationObservation(object): 93 | def __init__(self, station, observation): 94 | self.station = station 95 | self.observation = observation 96 | -------------------------------------------------------------------------------- /noaa/observation.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import noaa.exceptions 4 | import noaa.geocode 5 | import noaa.models 6 | import noaa.stations 7 | import noaa.utils 8 | 9 | 10 | def compiled_observation_for_location(location, stations, radius=10.0, 11 | units="miles"): 12 | loc = noaa.geocode.geocode_location(location) 13 | compiled_observation = compiled_observation_for_lat_lon( 14 | loc.lat, loc.lon, stations, radius=radius, units=units) 15 | return compiled_observation 16 | 17 | 18 | def compiled_observation_for_lat_lon(lat, lon, stations, radius=10.0, 19 | units="miles"): 20 | """Since some NOAA stations may not provide all of the data, this function 21 | attempts to search nearby Stations to create a single Observation 22 | that contains as much information as possible. 23 | """ 24 | compiled_observation = None 25 | for station_observation in nearby_station_observations_for_lat_lon( 26 | lat, lon, stations, radius=radius, units=units): 27 | 28 | if not compiled_observation: 29 | compiled_observation = copy.copy(station_observation.observation) 30 | 31 | if not noaa.utils.any_none(compiled_observation.__dict__.values()): 32 | # If all the values are filled in, break out of loop 33 | break 34 | 35 | for attr, compiled_value in compiled_observation.__dict__.items(): 36 | if compiled_value is None: 37 | station_value = getattr(station_observation.observation, attr) 38 | setattr(compiled_observation, attr, station_value) 39 | 40 | return compiled_observation 41 | 42 | 43 | def nearby_station_observations_for_location(location, stations): 44 | loc = noaa.geocode.geocode_location(location) 45 | station_observations = nearby_station_observations_for_lat_lon( 46 | loc.lat, loc.lon, stations) 47 | return station_observations 48 | 49 | 50 | def nearby_station_observations_for_lat_lon(lat, lon, stations, radius=10.0, 51 | units="miles"): 52 | station_observations = [] 53 | for dist, station in noaa.stations.nearest_stations_with_distance( 54 | lat, lon, stations, radius=radius, units=units): 55 | station_observation = station_observation_by_station_id( 56 | station.station_id) 57 | station_observations.append(station_observation) 58 | 59 | return station_observations 60 | 61 | 62 | def station_observation_by_station_id(station_id): 63 | """ 64 | Unfortunately not all NOAA stations return the same metrics: one station 65 | may return temp_f while another may return windchill_f. 66 | 67 | To work around this, we scan all stations within a radius of the target 68 | and return the observation (if any) that has the data we want. 69 | """ 70 | STATION_OBSERVATIONS_URL = ( 71 | 'http://www.weather.gov/data/current_obs/%s.xml' % station_id) 72 | 73 | resp = noaa.utils.open_url(STATION_OBSERVATIONS_URL) 74 | tree = noaa.utils.parse_xml(resp) 75 | 76 | station = _parse_station(tree) 77 | observation = _parse_observation(tree) 78 | station_observation = noaa.models.StationObservation(station, observation) 79 | return station_observation 80 | 81 | 82 | def _parse_station(tree): 83 | root = tree.getroot() 84 | 85 | lat = float(root.find("latitude").text) 86 | lon = float(root.find("longitude").text) 87 | loc_desc = root.find("location").text 88 | location = noaa.models.Location(lat, lon, loc_desc) 89 | 90 | station_id = root.find("station_id").text 91 | station = noaa.models.Station(station_id, location) 92 | return station 93 | 94 | 95 | def _parse_observation(tree): 96 | root = tree.getroot() 97 | updated_at = noaa.utils.parse_dt(root.find("observation_time_rfc822").text) 98 | 99 | temp_e = root.find("temp_f") 100 | if temp_e is None: 101 | temp = None 102 | else: 103 | temp = noaa.models.Temperature(float(temp_e.text), unit='F') 104 | 105 | observation = noaa.models.Observation(updated_at, temp) 106 | return observation 107 | -------------------------------------------------------------------------------- /noaa/stations.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import noaa.models 5 | import noaa.utils 6 | 7 | 8 | def nearest_stations_with_distance(lat, lon, stations, radius=10.0, 9 | units="miles"): 10 | """Find all stations within radius of target. 11 | 12 | :param lat: 13 | :param lon: 14 | :param stations: list of stations objects to scan 15 | :param radius: 16 | :param units: 17 | :returns: [(dist, station)] 18 | """ 19 | matches = [] 20 | for station in stations: 21 | s_lat = station.location.lat 22 | s_lon = station.location.lon 23 | dist = noaa.utils.earth_distance( 24 | s_lat, s_lon, lat, lon, dist_units=units) 25 | if dist <= radius: 26 | matches.append((dist, station)) 27 | 28 | matches.sort() 29 | return matches 30 | 31 | 32 | def nearest_station(lat, lon, stations): 33 | """Find single nearest station. 34 | 35 | :param lat: 36 | :param lon: 37 | :param stations: list of stations objects to scan 38 | """ 39 | matches = nearest_stations_with_distance(lat, lon, stations) 40 | if matches: 41 | dist, station = matches[0] 42 | else: 43 | station = None 44 | 45 | return station 46 | 47 | 48 | def get_stations_from_cache(filename): 49 | if not os.path.exists(filename): 50 | resp = noaa.stations.fetch_station_data() 51 | with open(filename, "w") as f: 52 | shutil.copyfileobj(resp, f) 53 | 54 | stations = noaa.stations.get_stations_from_file(filename) 55 | return stations 56 | 57 | 58 | def get_stations_from_web(): 59 | resp = fetch_station_data() 60 | stations = _parse_stations(resp) 61 | return stations 62 | 63 | 64 | def get_stations_from_file(filename): 65 | with open(filename) as f: 66 | stations = _parse_stations(f) 67 | return stations 68 | 69 | 70 | def fetch_station_data(): 71 | STATIONS_URL = "http://www.weather.gov/xml/current_obs/index.xml" 72 | resp = noaa.utils.open_url(STATIONS_URL) 73 | return resp 74 | 75 | 76 | def _parse_stations(fileobj): 77 | stations = [] 78 | tree = noaa.utils.parse_xml(fileobj) 79 | for station_e in tree.getroot().findall('station'): 80 | lat = float(station_e.find('latitude').text) 81 | lon = float(station_e.find('longitude').text) 82 | description = station_e.find('state').text 83 | location = noaa.models.Location(lat, lon, description) 84 | 85 | station_id = station_e.find('station_id').text 86 | station = noaa.models.Station(station_id, location) 87 | 88 | stations.append(station) 89 | 90 | return stations 91 | -------------------------------------------------------------------------------- /noaa/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import math 3 | import sys 4 | import urllib 5 | from xml.etree import ElementTree as ET 6 | 7 | import dateutil.parser 8 | 9 | 10 | def colorize(text, color): 11 | if color == "default": 12 | return text 13 | 14 | COLORS = {"black": 30, 15 | "red": 31, 16 | "green": 32, 17 | "yellow": 33, 18 | "blue": 34, 19 | "magenta": 35, 20 | "cyan": 36, 21 | "white": 37} 22 | 23 | color_code = COLORS[color] 24 | return "\x1b[%(color_code)s;1m%(text)s\x1b[0m" % locals() 25 | 26 | 27 | def any_none(L): 28 | return any(map(lambda x: x is None, L)) 29 | 30 | 31 | def all_numbers(L): 32 | try: 33 | map(float, L) 34 | return True 35 | except ValueError: 36 | return False 37 | 38 | 39 | def print_tree(tree, indent=4): 40 | """Print an ElementTree for debugging purposes.""" 41 | def print_elem(elem, level): 42 | print " " * (indent * level), 43 | print 'tag="%s"' % elem.tag, 44 | print 'text="%s"' % elem.text.strip() if elem.text is not None else "", 45 | print 'attrib=%s' % elem.attrib 46 | for child in elem.getchildren(): 47 | print_elem(child, level + 1) 48 | print_elem(tree.getroot(), 0) 49 | 50 | 51 | def open_url(url, params=None): 52 | if params: 53 | query_string = urllib.urlencode(params) 54 | url = "?".join([url, query_string]) 55 | 56 | resp = urllib.urlopen(url) 57 | return resp 58 | 59 | 60 | @contextlib.contextmanager 61 | def die_on(*exception_classes, **kwargs): 62 | """Print error message and exit the program with non-zero status if 63 | a matching exception is raised. 64 | 65 | :param msg_func: A function to generate the error message. 66 | :param exit_code: Exit code 67 | :returns: Nothing 68 | """ 69 | msg_func = kwargs.pop('msg_func', lambda e: "Error: %s" % e) 70 | exit_code = kwargs.pop('exit_code', 1) 71 | 72 | try: 73 | yield 74 | except exception_classes as e: 75 | print >> sys.stderr, msg_func(e) 76 | sys.exit(exit_code) 77 | 78 | 79 | def parse_xml(fileobj): 80 | tree = ET.parse(fileobj) 81 | return tree 82 | 83 | 84 | def parse_dt(dt): 85 | return dateutil.parser.parse(dt) 86 | 87 | 88 | def great_circle_distance(lat1, lon1, lat2, lon2, radius, angle_units="deg"): 89 | """see http://en.wikipedia.org/wiki/Haversine_formula""" 90 | asin = math.asin 91 | cos = math.cos 92 | radians = math.radians 93 | sqrt = math.sqrt 94 | 95 | def hsin(theta): 96 | return math.sin(float(theta) / 2) ** 2 97 | 98 | if angle_units == "deg": 99 | lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) 100 | elif angle_units == "rad": 101 | pass 102 | else: 103 | raise Exception("Unknown angle_units '%s'" % angle_units) 104 | 105 | dist = 2 * radius * asin( 106 | sqrt(hsin(lat2 - lat1) + ( 107 | cos(lat1) * cos(lat2) * hsin(lon2 - lon1)))) 108 | return dist 109 | 110 | 111 | def earth_distance(lat1, lon1, lat2, lon2, angle_units="deg", 112 | dist_units="miles"): 113 | 114 | EARTH_RADIUS = { 115 | "miles": 3963.1676, 116 | "km": 6378.1 117 | } 118 | 119 | try: 120 | radius = EARTH_RADIUS[dist_units] 121 | except KeyError: 122 | raise Exception("Unknown dist_units '%s'" % dist_units) 123 | 124 | return great_circle_distance(lat1, lon1, lat2, lon2, radius, 125 | angle_units=angle_units) 126 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from setuptools.command.sdist import sdist 3 | 4 | import noaa 5 | 6 | setup( 7 | name='noaa', 8 | version=noaa.__version__, 9 | description='Python Bindings to the NOAA National Digital Forecast ' 10 | 'Database (NDFD)', 11 | license='MIT License', 12 | author='Rick Harris', 13 | author_email='rconradharris@gmail.com', 14 | url='https://github.com/rconradharris/python-noaa', 15 | packages=find_packages(), 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: POSIX :: Linux', 20 | 'Programming Language :: Python :: 2.6' 21 | ], 22 | install_requires=[], 23 | scripts=["bin/noaa"]) 24 | -------------------------------------------------------------------------------- /tools/generate_authors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git shortlog -se | cut -c8- 3 | -------------------------------------------------------------------------------- /tools/pip-requires: -------------------------------------------------------------------------------- 1 | argparse 2 | dateutils 3 | pep8 4 | -------------------------------------------------------------------------------- /tools/run_pep8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pep8 --repeat noaa bin/noaa 3 | --------------------------------------------------------------------------------