diff --git a/metar.py b/metar.py new file mode 100644 index 0000000..1f12793 --- /dev/null +++ b/metar.py @@ -0,0 +1,280 @@ +import datetime + +INTENSITY = { + "-": "light", + "+": "heavy", + "VC": "in the vicinity:", +} + +DESCRIPTOR = { + "MI": "shallow", + "PR": "partial", + "BC": "patches", + "DR": "low drifting", + "BL": "blowing", + "SH": "showers", + "TS": "thunderstorm", + "FZ": "freezing", +} + +PRECIPITATION = { + "DZ": "drizzle", + "RA": "rain", + "SN": "snow", + "SG": "snow grains", + "IC": "ice crystals", + "PL": "ice pellets", + "GR": "hail", + "GS": "small hail", + "UP": "unknown precipitation", +} + +OBSCURATION = { + "BR": "mist", + "FG": "fog", + "VA": "volcanic ash", + "DU": "widespread dust", + "SA": "sand", + "HZ": "haze", + "PY": "spray", +} + +CLOUD_COVER = { + "SKC": "clear", + "CLR": "clear", + "NSC": "clear", + "FEW": "a few clouds", + "SCT": "scattered clouds", + "BKN": "broken clouds", + "OVC": "overcast", + "VV": "indefinite ceiling", +} + +OTHER = { + "PO": "whirls", + "SQ": "squals", + "FC": "tornado", + "SS": "sandstorm", + "DS": "duststorm", +} + +import re + + +class Weather(object): + cover = None + height = None + wind_speed = None + wind_direction = None + intensity = None + descriptor = None + precipitation = None + obscuration = None + other = None + conditions = None + + def describe_wind(self): + if self.wind_speed is not None: + if self.wind_speed < 1: + return "calm" + elif self.wind_speed < 4: + return "light air" + elif self.wind_speed < 7: + return "light breeze" + elif self.wind_speed < 11: + return "gentle breeze" + elif self.wind_speed < 16: + return "moderate breeze" + elif self.wind_speed < 22: + return "fresh breeze" + elif self.wind_speed < 28: + return "strong breeze" + elif self.wind_speed < 34: + return "near gale" + elif self.wind_speed < 41: + return "gale" + elif self.wind_speed < 56: + return "storm" + elif self.wind_speed < 64: + return "violent storm" + else: + return "hurricane" + else: + return 'unknown' + + def windsock(self): + if self.wind_direction is not None: + if (self.wind_speed <= 22.5) or (self.wind_speed > 337.5): + return '\u2191' + elif (self.wind_speed > 22.5) and (self.wind_speed <= 67.5): + return '\u2197' + elif (self.wind_speed > 67.5) and (self.wind_speed <= 112.5): + return '\u2192' + elif (self.wind_speed > 112.5) and (self.wind_speed <= 157.5): + return '\u2198' + elif (self.wind_speed > 157.5) and (self.wind_speed <= 202.5): + return '\u2193' + elif (self.wind_speed > 202.5) and (self.wind_speed <= 247.5): + return '\u2199' + elif (self.wind_speed > 247.5) and (self.wind_speed <= 292.5): + return '\u2190' + elif (self.wind_speed > 292.5) and (self.wind_speed <= 337.5): + return '\u2196' + else: + return '?' + + def __repr__(self): + if self.conditions: + ret = "{cover}, {temperature}°C, {pressure} hPa, {conditions}, "\ + "{windnote} {wind} m/s ({windsock}) - {station} {time}" + else: + ret = "{cover}, {temperature}°C, {pressure} hPa, "\ + "{windnote} {wind} m/s ({windsock}) - {station} {time}" + + wind = self.wind_speed if self.wind_speed is not None else '?' + + return ret.format(cover=self.cover, temperature=self.temperature, + pressure=self.pressure, conditions=self.conditions, + wind=wind, windnote=self.describe_wind(), + windsock=self.windsock(), station=self.station, + time=self.time.strftime("%H:%MZ")) + + +def build_regex(key, classifier): + ret = "|".join([re.escape(x) for x in classifier.keys()]) + return r"(?P<{key}>{regex})".format(key=re.escape(key), regex=ret) + + +def weather_regex(): + ret = r'\s' + ret += build_regex('intensity', INTENSITY) + r'?' + ret += build_regex('descriptor', DESCRIPTOR) + r'?' + ret += build_regex('precipitation', PRECIPITATION) + r'?' + ret += build_regex('obscuration', OBSCURATION) + r'?' + ret += build_regex('other', OTHER) + r'?' + ret += r'\s' + return re.compile(ret) + + +def parse_temp(t): + if t[0] == 'M': + return -int(t[1:]) + return int(t) + + +def parse(data): + w = Weather() + + data = data.splitlines() + metar = data[1].split() + + w.metar = data[1] + w.station = metar[0] + metar = metar[1:] + + # time + time_re = re.compile(r"\d{2}(?P\d{2})(?P\d{2})Z") + m = time_re.search(w.metar) + if m: + w.time = datetime.time(hour=int(m.group('hour')), + minute=int(m.group('min'))) + + # mode + #if metar[0] == "AUTO": + # metar = metar[1:] + + # wind speed + wind_re = re.compile(r"(?P\d{3})(?P\d+)(G(?P\d+))?(?PKT|MPS)") + m = wind_re.search(w.metar) + if m: + w.wind_direction = int(m.group('direction')) + + if m.group('unit') == "KT": + # convert knots to m/s + w.wind_speed = round(int(m.group('speed')) * 1852 / 3600) + if m.group('gust'): + w.wind_gust = round(int(m.group('speed')) * 1852 / 3600) + else: + w.wind_gust = None + else: + w.wind_speed = int(m.group('speed')) + if m.group('gust'): + w.wind_gust = int(m.group('gust')) + else: + w.wind_gust = None + metar = metar[1:] + + # visibility + # 0800N? + visibility_re = re.compile(r"(?P(?P\d+)SM|(?P\d{4})\s|CAVOK)") + m = visibility_re.search(w.metar) + if m: + if m.group('dist'): + w.visibility = m.group('dist') + elif m.group('disti'): + w.visibility = m.group('disti') + elif m.group('vis') == 'CAVOK': + w.cover = "clear" + w.visibility = m.group('vis') + else: + w.visibility = None + + # runway visibility range + + # conditions + matches = weather_regex().finditer(w.metar) + for m in matches: + if not m: + continue + + weather = [] + if m.group('intensity'): + w.intensity = INTENSITY[m.group('intensity')] + weather.append(w.intensity) + if m.group('descriptor'): + w.descriptor = DESCRIPTOR[m.group('descriptor')] + weather.append(w.descriptor) + if m.group('precipitation'): + w.precipitation = PRECIPITATION[m.group('precipitation')] + weather.append(w.precipitation) + if m.group('obscuration'): + w.obscuration = OBSCURATION[m.group('obscuration')] + weather.append(w.obscuration) + if m.group('other'): + w.other = OTHER[m.group('other')] + weather.append(w.other) + if len(weather) > 0: + w.conditions = " ".join(weather) + + # cloud cover + cover_re = re.compile(build_regex('cover', CLOUD_COVER) +\ + r"(?P\d*)") + matches = cover_re.finditer(w.metar) + for m in matches: + w.cover = CLOUD_COVER[m.group('cover')] + w.height = m.group('height') + + # temperature + temp_re = re.compile(r"(?P[M\d]+)\/(?P[M\d]+)") + m = temp_re.search(w.metar) + if m: + w.temperature = parse_temp(m.group('temp')) + w.dewpoint = parse_temp(m.group('dewpoint')) + + # pressure + pressure_re = re.compile(r"([QA])(\d+)") + m = pressure_re.search(w.metar) + if m.group(1) == 'A': + # convert inHg to hPa + w.pressure = round(int(m.group(2)) / 100 * 3.386389) + else: + w.pressure = int(m.group(2)) + + return w + + +if __name__ == "__main__": + import glob + for station in glob.glob('test/metar/*.TXT'): + with open(station) as f: + print(parse(f.read())) diff --git a/modules/test/test_weather.py b/modules/test/test_weather.py index 874b087..52a83b2 100644 --- a/modules/test/test_weather.py +++ b/modules/test/test_weather.py @@ -27,27 +27,21 @@ class TestWeather(unittest.TestCase): self.assertEqual(icao, 'KIAD') def test_airport(self): - input = Mock( - match=Mock(group=lambda x: 'KIAD'), - sender='#phenny', nick='phenny_test') + input = Mock(group=lambda x: 'KIAD') f_weather(self.phenny, input) - assert self.phenny.msg.called is True + assert self.phenny.say.called is True def test_place(self): - input = Mock( - match=Mock(group=lambda x: 'Blacksburg'), - sender='#phenny', nick='phenny_test') + input = Mock(group=lambda x: 'Blacksburg') f_weather(self.phenny, input) - assert self.phenny.msg.called is True + assert self.phenny.say.called is True def test_notfound(self): - input = Mock( - match=Mock(group=lambda x: 'Hell'), - sender='#phenny', nick='phenny_test') + input = Mock(group=lambda x: 'Hell') f_weather(self.phenny, input) - self.phenny.msg.called_once_with('#phenny', + self.phenny.say.called_once_with('#phenny', "No NOAA data available for that location.") diff --git a/modules/weather.py b/modules/weather.py index 174795b..0fd78bc 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -8,15 +8,16 @@ http://inamidst.com/phenny/ """ import re, urllib.request, urllib.parse, urllib.error +import metar import web from tools import deprecated, GrumbleError r_from = re.compile(r'(?i)([+-]\d+):00 from') -def location(name): +def location(name): name = urllib.parse.quote(name) uri = 'http://ws.geonames.org/searchJSON?q=%s&maxRows=1' % name - for i in range(10): + for i in range(10): bytes = web.get(uri) if bytes is not None: break @@ -29,14 +30,14 @@ def location(name): lng = results['geonames'][0]['lng'] return name, countryName, lat, lng -def local(icao, hour, minute): +def local(icao, hour, minute): uri = ('http://www.flightstats.com/' + 'go/Airport/airportDetails.do?airportCode=%s') try: bytes = web.get(uri % icao) - except AttributeError: + except AttributeError: raise GrumbleError('A WEBSITE HAS GONE DOWN WTF STUPID WEB') m = r_from.search(bytes) - if m: + if m: offset = m.group(1) lhour = int(hour) + int(offset) lhour = lhour % 24 @@ -46,7 +47,7 @@ def local(icao, hour, minute): # ':' + str(minute) + 'Z)') return str(hour) + ':' + str(minute) + 'Z' -def code(phenny, search): +def code(phenny, search): from icao import data if search.upper() in [loc[0] for loc in data]: @@ -55,358 +56,41 @@ def code(phenny, search): name, country, latitude, longitude = location(search) if name == '?': return False sumOfSquares = (99999999999999999999999999999, 'ICAO') - for icao_code, lat, lon in data: + for icao_code, lat, lon in data: latDiff = abs(latitude - lat) lonDiff = abs(longitude - lon) diff = (latDiff * latDiff) + (lonDiff * lonDiff) - if diff < sumOfSquares[0]: + if diff < sumOfSquares[0]: sumOfSquares = (diff, icao_code) return sumOfSquares[1] -@deprecated -def f_weather(self, origin, match, args): +def f_weather(phenny, input): """.weather - Show the weather at airport with the code .""" - if origin.sender == '#talis': - if args[0].startswith('.weather '): return + icao_code = input.group(2) + if not icao_code: + return phenny.say("Try .weather London, for example?") - icao_code = match.group(2) - if not icao_code: - return self.msg(origin.sender, 'Try .weather London, for example?') + icao_code = code(phenny, icao_code) - icao_code = code(self, icao_code) - - if not icao_code: - self.msg(origin.sender, 'No ICAO code found, sorry') + if not icao_code: + phenny.say("No ICAO code found, sorry") return uri = 'http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT' - try: bytes = web.get(uri % icao_code) - except AttributeError: + try: + bytes = web.get(uri % icao_code) + except AttributeError: raise GrumbleError('OH CRAP NOAA HAS GONE DOWN THE WEB IS BROKEN') except urllib.error.HTTPError: - self.msg(origin.sender, "No NOAA data available for that location.") + phenny.say("No NOAA data available for that location.") return - if 'Not Found' in bytes: - self.msg(origin.sender, icao_code+': no such ICAO code, or no NOAA data') + if 'Not Found' in bytes: + phenny.say(icao_code + ": no such ICAO code, or no NOAA data") return - metar = bytes.splitlines().pop() - metar = metar.split(' ') - - if len(metar[0]) == 4: - metar = metar[1:] - - if metar[0].endswith('Z'): - time = metar[0] - metar = metar[1:] - else: time = None - - if metar[0] == 'AUTO': - metar = metar[1:] - if metar[0] == 'VCU': - self.msg(origin.sender, icao_code + ': no data provided') - return - - if metar[0].endswith('KT'): - wind = metar[0] - metar = metar[1:] - else: wind = None - - if ('V' in metar[0]) and (metar[0] != 'CAVOK'): - vari = metar[0] - metar = metar[1:] - else: vari = None - - if ((len(metar[0]) == 4) or - metar[0].endswith('SM')): - visibility = metar[0] - metar = metar[1:] - else: visibility = None - - while metar[0].startswith('R') and (metar[0].endswith('L') - or 'L/' in metar[0]): - metar = metar[1:] - - if len(metar[0]) == 6 and (metar[0].endswith('N') or - metar[0].endswith('E') or - metar[0].endswith('S') or - metar[0].endswith('W')): - metar = metar[1:] # 7000SE? - - cond = [] - while (((len(metar[0]) < 5) or - metar[0].startswith('+') or - metar[0].startswith('-')) and (not (metar[0].startswith('VV') or - metar[0].startswith('SKC') or metar[0].startswith('CLR') or - metar[0].startswith('FEW') or metar[0].startswith('SCT') or - metar[0].startswith('BKN') or metar[0].startswith('OVC')))): - cond.append(metar[0]) - metar = metar[1:] - - while '/P' in metar[0]: - metar = metar[1:] - - if not metar: - self.msg(origin.sender, icao_code + ': no data provided') - return - - cover = [] - while (metar[0].startswith('VV') or metar[0].startswith('SKC') or - metar[0].startswith('CLR') or metar[0].startswith('FEW') or - metar[0].startswith('SCT') or metar[0].startswith('BKN') or - metar[0].startswith('OVC')): - cover.append(metar[0]) - metar = metar[1:] - if not metar: - self.msg(origin.sender, icao_code + ': no data provided') - return - - if metar[0] == 'CAVOK': - cover.append('CLR') - metar = metar[1:] - - if metar[0] == 'PRFG': - cover.append('CLR') # @@? - metar = metar[1:] - - if metar[0] == 'NSC': - cover.append('CLR') - metar = metar[1:] - - if ('/' in metar[0]) or (len(metar[0]) == 5 and metar[0][2] == '.'): - temp = metar[0] - metar = metar[1:] - else: temp = None - - if metar[0].startswith('QFE'): - metar = metar[1:] - - if metar[0].startswith('Q') or metar[0].startswith('A'): - pressure = metar[0] - metar = metar[1:] - else: pressure = None - - if time: - hour = time[2:4] - minute = time[4:6] - time = local(icao_code, hour, minute) - else: time = '(time unknown)' - - if wind: - speed = int(wind[3:5]) - if speed < 1: - description = 'Calm' - elif speed < 4: - description = 'Light air' - elif speed < 7: - description = 'Light breeze' - elif speed < 11: - description = 'Gentle breeze' - elif speed < 16: - description = 'Moderate breeze' - elif speed < 22: - description = 'Fresh breeze' - elif speed < 28: - description = 'Strong breeze' - elif speed < 34: - description = 'Near gale' - elif speed < 41: - description = 'Gale' - elif speed < 48: - description = 'Strong gale' - elif speed < 56: - description = 'Storm' - elif speed < 64: - description = 'Violent storm' - else: description = 'Hurricane' - - if wind[0:3] == 'VRB': - degrees = '\u21BB' - else: - degrees = float(wind[0:3]) - if (degrees <= 22.5) or (degrees > 337.5): - degrees = '\u2191' - elif (degrees > 22.5) and (degrees <= 67.5): - degrees = '\u2197' - elif (degrees > 67.5) and (degrees <= 112.5): - degrees = '\u2192' - elif (degrees > 112.5) and (degrees <= 157.5): - degrees = '\u2198' - elif (degrees > 157.5) and (degrees <= 202.5): - degrees = '\u2193' - elif (degrees > 202.5) and (degrees <= 247.5): - degrees = '\u2199' - elif (degrees > 247.5) and (degrees <= 292.5): - degrees = '\u2190' - elif (degrees > 292.5) and (degrees <= 337.5): - degrees = '\u2196' - - if not icao_code.startswith('EN') and not icao_code.startswith('ED'): - wind = '%s %skt (%s)' % (description, speed, degrees) - elif icao_code.startswith('ED'): - kmh = int(round(speed * 1.852, 0)) - wind = '%s %skm/h (%skt) (%s)' % (description, kmh, speed, degrees) - elif icao_code.startswith('EN'): - ms = int(round(speed * 0.514444444, 0)) - wind = '%s %sm/s (%skt) (%s)' % (description, ms, speed, degrees) - else: wind = '(wind unknown)' - - if visibility: - visibility = visibility + 'm' - else: visibility = '(visibility unknown)' - - if cover: - level = None - for c in cover: - if c.startswith('OVC') or c.startswith('VV'): - if (level is None) or (level < 8): - level = 8 - elif c.startswith('BKN'): - if (level is None) or (level < 5): - level = 5 - elif c.startswith('SCT'): - if (level is None) or (level < 3): - level = 3 - elif c.startswith('FEW'): - if (level is None) or (level < 1): - level = 1 - elif c.startswith('SKC') or c.startswith('CLR'): - if level is None: - level = 0 - - if level == 8: - cover = 'Overcast \u2601' - elif level == 5: - cover = 'Cloudy' - elif level == 3: - cover = 'Scattered' - elif (level == 1) or (level == 0): - cover = 'Clear \u263C' - else: cover = 'Cover Unknown' - else: cover = 'Cover Unknown' - - if temp: - if '/' in temp: - temp = temp.split('/')[0] - else: temp = temp.split('.')[0] - if temp.startswith('M'): - temp = '-' + temp[1:] - try: temp = int(temp) - except ValueError: temp = '?' - else: temp = '?' - - if pressure: - if pressure.startswith('Q'): - pressure = pressure.lstrip('Q') - if pressure != 'NIL': - pressure = str(int(pressure)) + 'mb' - else: pressure = '?mb' - elif pressure.startswith('A'): - pressure = pressure.lstrip('A') - if pressure != 'NIL': - inches = pressure[:2] + '.' + pressure[2:] - mb = int(float(inches) * 33.7685) - pressure = '%sin (%smb)' % (inches, mb) - else: pressure = '?mb' - - if isinstance(temp, int): - f = round((temp * 1.8) + 32, 2) - temp = '%s\u2109 (%s\u2103)' % (f, temp) - else: pressure = '?mb' - if isinstance(temp, int): - temp = '%s\u2103' % temp - - if cond: - conds = cond - cond = '' - - intensities = { - '-': 'Light', - '+': 'Heavy' - } - - descriptors = { - 'MI': 'Shallow', - 'PR': 'Partial', - 'BC': 'Patches', - 'DR': 'Drifting', - 'BL': 'Blowing', - 'SH': 'Showers of', - 'TS': 'Thundery', - 'FZ': 'Freezing', - 'VC': 'In the vicinity:' - } - - phenomena = { - 'DZ': 'Drizzle', - 'RA': 'Rain', - 'SN': 'Snow', - 'SG': 'Snow Grains', - 'IC': 'Ice Crystals', - 'PL': 'Ice Pellets', - 'GR': 'Hail', - 'GS': 'Small Hail', - 'UP': 'Unknown Precipitation', - 'BR': 'Mist', - 'FG': 'Fog', - 'FU': 'Smoke', - 'VA': 'Volcanic Ash', - 'DU': 'Dust', - 'SA': 'Sand', - 'HZ': 'Haze', - 'PY': 'Spray', - 'PO': 'Whirls', - 'SQ': 'Squalls', - 'FC': 'Tornado', - 'SS': 'Sandstorm', - 'DS': 'Duststorm', - # ? Cf. http://swhack.com/logs/2007-10-05#T07-58-56 - 'TS': 'Thunderstorm', - 'SH': 'Showers' - } - - for c in conds: - if c.endswith('//'): - if cond: cond += ', ' - cond += 'Some Precipitation' - elif len(c) == 5: - intensity = intensities[c[0]] - descriptor = descriptors[c[1:3]] - phenomenon = phenomena.get(c[3:], c[3:]) - if cond: cond += ', ' - cond += intensity + ' ' + descriptor + ' ' + phenomenon - elif len(c) == 4: - descriptor = descriptors.get(c[:2], c[:2]) - phenomenon = phenomena.get(c[2:], c[2:]) - if cond: cond += ', ' - cond += descriptor + ' ' + phenomenon - elif len(c) == 3: - intensity = intensities.get(c[0], c[0]) - phenomenon = phenomena.get(c[1:], c[1:]) - if cond: cond += ', ' - cond += intensity + ' ' + phenomenon - elif len(c) == 2: - phenomenon = phenomena.get(c, c) - if cond: cond += ', ' - cond += phenomenon - - # if not cond: - # format = u'%s at %s: %s, %s, %s, %s' - # args = (icao, time, cover, temp, pressure, wind) - # else: - # format = u'%s at %s: %s, %s, %s, %s, %s' - # args = (icao, time, cover, temp, pressure, cond, wind) - - if not cond: - format = '%s, %s, %s, %s - %s %s' - args = (cover, temp, pressure, wind, str(icao_code), time) - else: - format = '%s, %s, %s, %s, %s - %s, %s' - args = (cover, temp, pressure, cond, wind, str(icao_code), time) - - self.msg(origin.sender, format % args) + phenny.say(str(metar.parse(bytes))) f_weather.rule = (['weather'], r'(.*)') -if __name__ == '__main__': +if __name__ == '__main__': print(__doc__.strip()) diff --git a/test/metar/CYUX.TXT b/test/metar/CYUX.TXT new file mode 100644 index 0000000..224f45b --- /dev/null +++ b/test/metar/CYUX.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:28 +CYUX 110028Z AUTO 25010KT 6SM -SN BKN043 OVC065 M28/M31 A2975 RMK SLP077 diff --git a/test/metar/DNIM.TXT b/test/metar/DNIM.TXT new file mode 100644 index 0000000..2b32458 --- /dev/null +++ b/test/metar/DNIM.TXT @@ -0,0 +1,2 @@ +2012/03/31 08:00 +DNIM 310800Z 17005KT 9999 NSC 27/24 Q1013 diff --git a/test/metar/DXLK.TXT b/test/metar/DXLK.TXT new file mode 100644 index 0000000..f57056d --- /dev/null +++ b/test/metar/DXLK.TXT @@ -0,0 +1,2 @@ +2010/06/18 06:00 +DXLK 180600Z 28002KT 9999 FEW016 SCT120 BKN260 24/23 Q1013 diff --git a/test/metar/EDDF.TXT b/test/metar/EDDF.TXT new file mode 100644 index 0000000..1d8fbd5 --- /dev/null +++ b/test/metar/EDDF.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:20 +EDDF 110020Z 21005KT 9999 SCT020 BKN040 BKN070 02/01 Q1010 NOSIG diff --git a/test/metar/EDDH.TXT b/test/metar/EDDH.TXT new file mode 100644 index 0000000..7c0bd56 --- /dev/null +++ b/test/metar/EDDH.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:20 +EDDH 110020Z 32008KT 9999 FEW018 BKN050 M00/M02 Q1012 TEMPO 3500 SN BKN010 diff --git a/test/metar/EDDM.TXT b/test/metar/EDDM.TXT new file mode 100644 index 0000000..00230a6 --- /dev/null +++ b/test/metar/EDDM.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:20 +EDDM 110020Z 26008KT 9999 FEW012 SCT033 BKN045 02/01 Q1009 NOSIG diff --git a/test/metar/EDDT.TXT b/test/metar/EDDT.TXT new file mode 100644 index 0000000..bac3fff --- /dev/null +++ b/test/metar/EDDT.TXT @@ -0,0 +1,2 @@ +2013/01/10 21:50 +EDDT 102150Z 32007KT 9999 FEW014 BKN036 M00/M03 Q1008 TEMPO BKN012 diff --git a/test/metar/ENSO.TXT b/test/metar/ENSO.TXT new file mode 100644 index 0000000..38a8b59 --- /dev/null +++ b/test/metar/ENSO.TXT @@ -0,0 +1,2 @@ +2013/01/10 18:20 +ENSO 101820Z VRB01KT 9999 FEW025 00/M01 Q1017 diff --git a/test/metar/HEGN.TXT b/test/metar/HEGN.TXT new file mode 100644 index 0000000..94e2982 --- /dev/null +++ b/test/metar/HEGN.TXT @@ -0,0 +1,2 @@ +2013/01/11 01:00 +HEGN 110100Z 31008KT CAVOK 09/00 Q1025 NOSIG diff --git a/test/metar/KAXN.TXT b/test/metar/KAXN.TXT new file mode 100644 index 0000000..4c9058c --- /dev/null +++ b/test/metar/KAXN.TXT @@ -0,0 +1,2 @@ +2013/01/10 21:53 +KAXN 102153Z AUTO 16019G24KT 10SM BKN021 03/M06 A2989 RMK AO2 SLP137 T00331056 diff --git a/test/metar/KBCB.TXT b/test/metar/KBCB.TXT new file mode 100644 index 0000000..3872602 --- /dev/null +++ b/test/metar/KBCB.TXT @@ -0,0 +1,2 @@ +2013/01/10 20:15 +KBCB 102015Z AUTO 07004KT 10SM CLR 14/01 A3046 RMK AO2 diff --git a/test/metar/KBIL.TXT b/test/metar/KBIL.TXT new file mode 100644 index 0000000..07f7c95 --- /dev/null +++ b/test/metar/KBIL.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:20 +KBIL 110020Z 05017KT 10SM FEW019 OVC030 M03/M07 A2957 RMK AO2 T10281072 diff --git a/test/metar/KCID.TXT b/test/metar/KCID.TXT new file mode 100644 index 0000000..d8534b1 --- /dev/null +++ b/test/metar/KCID.TXT @@ -0,0 +1,2 @@ +2013/01/10 21:52 +KCID 102152Z 11013KT 3SM -RA BR OVC015 01/00 A2998 RMK AO2 RAB12 SLP162 P0001 T00110000 diff --git a/test/metar/KCXP.TXT b/test/metar/KCXP.TXT new file mode 100644 index 0000000..3c01289 --- /dev/null +++ b/test/metar/KCXP.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:35 +KCXP 110035Z AUTO 31006KT 10SM FEW043 M02/M13 A2990 RMK AO2 diff --git a/test/metar/KDEN.TXT b/test/metar/KDEN.TXT new file mode 100644 index 0000000..7e71b53 --- /dev/null +++ b/test/metar/KDEN.TXT @@ -0,0 +1,2 @@ +2013/01/10 21:53 +KDEN 102153Z 00000KT 10SM SCT150 OVC200 02/M07 A2959 RMK AO2 SLP007 T00221067 diff --git a/test/metar/KIAD.TXT b/test/metar/KIAD.TXT new file mode 100644 index 0000000..70596d9 --- /dev/null +++ b/test/metar/KIAD.TXT @@ -0,0 +1,2 @@ +2013/01/10 18:52 +KIAD 101852Z 35008KT 10SM FEW250 12/M04 A3052 RMK AO2 SLP335 T01221039 diff --git a/test/metar/KLAX.TXT b/test/metar/KLAX.TXT new file mode 100644 index 0000000..25b937c --- /dev/null +++ b/test/metar/KLAX.TXT @@ -0,0 +1,2 @@ +2013/01/10 23:53 +KLAX 102353Z 28025G31KT 10SM FEW070 SCT110 13/02 A2995 RMK AO2 PK WND 28033/2337 SLP141 T01280022 10150 20122 50002 diff --git a/test/metar/KLGA.TXT b/test/metar/KLGA.TXT new file mode 100644 index 0000000..e4383f9 --- /dev/null +++ b/test/metar/KLGA.TXT @@ -0,0 +1,2 @@ +2013/01/10 23:51 +KLGA 102351Z 31007KT 10SM SCT250 07/M04 A3052 RMK AO2 SLP335 T00721039 10089 20072 53014 diff --git a/test/metar/KMCO.TXT b/test/metar/KMCO.TXT new file mode 100644 index 0000000..357b1df --- /dev/null +++ b/test/metar/KMCO.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:53 +KMCO 110053Z 10005KT 10SM FEW250 20/18 A3029 RMK AO2 SLP255 T02000178 diff --git a/test/metar/KMGJ.TXT b/test/metar/KMGJ.TXT new file mode 100644 index 0000000..73c91c9 --- /dev/null +++ b/test/metar/KMGJ.TXT @@ -0,0 +1,2 @@ +2013/01/10 23:54 +KMGJ 102354Z AUTO 00000KT 10SM CLR 00/M04 A3051 RMK AO2 SLP336 T00001044 10078 21006 53014 TSNO diff --git a/test/metar/KMIA.TXT b/test/metar/KMIA.TXT new file mode 100644 index 0000000..669a387 --- /dev/null +++ b/test/metar/KMIA.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:53 +KMIA 110053Z 08011KT 10SM FEW060 SCT250 24/19 A3026 RMK AO2 SLP246 T02390194 diff --git a/test/metar/KSAN.TXT b/test/metar/KSAN.TXT new file mode 100644 index 0000000..ce71ea1 --- /dev/null +++ b/test/metar/KSAN.TXT @@ -0,0 +1,2 @@ +2013/01/10 21:51 +KSAN 102151Z 30017G22KT 10SM SCT040 BKN060 BKN180 13/01 A3003 RMK AO2 PK WND 28027/2140 SLP167 T01330011 diff --git a/test/metar/KSFO.TXT b/test/metar/KSFO.TXT new file mode 100644 index 0000000..b440ccc --- /dev/null +++ b/test/metar/KSFO.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:56 +KSFO 110056Z 29017KT 10SM FEW033 SCT049 09/02 A3012 RMK AO2 SLP199 T00940017 diff --git a/test/metar/LRAR.TXT b/test/metar/LRAR.TXT new file mode 100644 index 0000000..938daa6 --- /dev/null +++ b/test/metar/LRAR.TXT @@ -0,0 +1,2 @@ +2013/01/10 18:30 +LRAR 101830Z 11003KT 2000 0800N R27/1000VP2000D BCFG SCT005 M02/M02 Q1011 09890392 diff --git a/test/metar/MMUN.TXT b/test/metar/MMUN.TXT new file mode 100644 index 0000000..e697165 --- /dev/null +++ b/test/metar/MMUN.TXT @@ -0,0 +1,2 @@ +2013/01/10 23:44 +MMUN 102344Z 11007KT 7SM SCT015TCU SCT080 26/23 A3006 RMK SLP178 52010 906 8/230 HZY AS W diff --git a/test/metar/OEMM.TXT b/test/metar/OEMM.TXT new file mode 100644 index 0000000..63fc71b --- /dev/null +++ b/test/metar/OEMM.TXT @@ -0,0 +1,2 @@ +2010/07/15 11:00 +OEMM 151100Z 02009KT CAVOK 48/01 Q0997 diff --git a/test/metar/TBOB.TXT b/test/metar/TBOB.TXT new file mode 100644 index 0000000..929568f --- /dev/null +++ b/test/metar/TBOB.TXT @@ -0,0 +1,2 @@ +2011/06/09 09:00 +TBOB 090900Z 11006KT 9999 SCT014 SCT038 28/25 Q1014 NOSIG diff --git a/test/metar/UUOK.TXT b/test/metar/UUOK.TXT new file mode 100644 index 0000000..658e9e9 --- /dev/null +++ b/test/metar/UUOK.TXT @@ -0,0 +1,2 @@ +2013/01/10 16:00 +UUOK 101600Z 16004MPS 9999 OVC013 M13/M16 Q1014 NOSIG RMK 12CLRD60 diff --git a/test/metar/YSSY.TXT b/test/metar/YSSY.TXT new file mode 100644 index 0000000..2d5716e --- /dev/null +++ b/test/metar/YSSY.TXT @@ -0,0 +1,2 @@ +2013/01/11 00:30 +YSSY 110030Z 05007KT 350V080 CAVOK 28/17 Q1007 NOSIG diff --git a/test/metar/ZBDT.TXT b/test/metar/ZBDT.TXT new file mode 100644 index 0000000..736f940 --- /dev/null +++ b/test/metar/ZBDT.TXT @@ -0,0 +1,2 @@ +2008/03/23 23:00 +ZBDT 232300Z 333004MPS CAVOK M04/M14 Q1020 NOSIG diff --git a/test/metar/ZPLJ.TXT b/test/metar/ZPLJ.TXT new file mode 100644 index 0000000..7951316 --- /dev/null +++ b/test/metar/ZPLJ.TXT @@ -0,0 +1,2 @@ +2012/09/28 04:00 +ZPLJ 280400Z 24002MPS 210V290 9999 -SHRA FEW023 FEW040TCU SCT040 19/15 Q1026 NOSIG diff --git a/test/test_metar.py b/test/test_metar.py new file mode 100644 index 0000000..9e6b9e5 --- /dev/null +++ b/test/test_metar.py @@ -0,0 +1,25 @@ +""" +Tests for phenny's metar.py +""" + +import unittest +import metar +import glob + + +class MetarTest(unittest.TestCase): + def test_files(self): + for station in glob.glob('test/metar/*.TXT'): + with open(station) as f: + w = metar.parse(f.read()) + assert w.station is not None + assert w.time is not None + assert w.cover is not None + + assert w.temperature > -100 + assert w.temperature < 100 + + assert w.dewpoint > -100 + assert w.dewpoint < 100 + + assert w.pressure is not None