def test_taf_line(self): """ Tests converting TAF line data into into a single spoken string """ units = structs.Units(**static.NA_UNITS) line = { 'altimeter': core.make_number('2992'), 'clouds': [core.make_cloud('BKN015CB')], 'end_time': core.make_timestamp('1206'), 'icing': ['611005'], 'other': ['+RA'], 'start_time': core.make_timestamp('1202'), 'turbulance': ['540553'], 'type': 'FROM', 'visibility': core.make_number('3'), 'wind_direction': core.make_number('360'), 'wind_gust': core.make_number('20'), 'wind_shear': 'WS020/07040KT', 'wind_speed': core.make_number('12'), } line.update({ k: None for k in ('flight_rules', 'probability', 'raw', 'sanitized') }) line = structs.TafLineData(**line) spoken = ( 'From 2 to 6 zulu, Winds three six zero at 12kt gusting to 20kt. ' 'Wind shear 2000inHg from zero seven zero at 40kt. Visibility three miles. ' 'Altimeter two nine point nine two. Heavy Rain. ' 'Broken layer at 1500ft (Cumulonimbus). ' 'Occasional moderate turbulence in clouds from 5500ft to 8500ft. ' 'Light icing from 10000ft to 15000ft') ret = speech.taf_line(line, units) self.assertIsInstance(ret, str) self.assertEqual(ret, spoken)
def test_taf_line(self): """ Tests converting TAF line data into into a single spoken string """ units = structs.Units(**static.NA_UNITS) line = { 'altimeter': core.make_number('2992'), 'clouds': [core.make_cloud('BKN015CB')], 'end_time': core.make_timestamp('1206'), 'icing': ['611005'], 'other': ['+RA'], 'start_time': core.make_timestamp('1202'), 'turbulance': ['540553'], 'type': 'FROM', 'visibility': core.make_number('3'), 'wind_direction': core.make_number('360'), 'wind_gust': core.make_number('20'), 'wind_shear': 'WS020/07040KT', 'wind_speed': core.make_number('12'), } line.update({k: None for k in ('flight_rules', 'probability', 'raw', 'sanitized')}) line = structs.TafLineData(**line) spoken = ('From 2 to 6 zulu, Winds three six zero at 12kt gusting to 20kt. ' 'Wind shear 2000inHg from zero seven zero at 40kt. Visibility three miles. ' 'Altimeter two nine point nine two. Heavy Rain. ' 'Broken layer at 1500ft (Cumulonimbus). ' 'Occasional moderate turbulence in clouds from 5500ft to 8500ft. ' 'Light icing from 10000ft to 15000ft') ret = speech.taf_line(line, units) self.assertIsInstance(ret, str) self.assertEqual(ret, spoken)
def parse_in(txt: str) -> (MetarData, Units): """ Parser for the International METAR variant """ units = Units(**IN_UNITS) clean = core.sanitize_report_string(txt) wxresp = {'raw': txt, 'sanitized': clean} wxdata, wxresp['remarks'] = core.get_remarks(clean) wxdata, wxresp['runway_visibility'], _ = core.sanitize_report_list(wxdata) wxdata, wxresp['station'], wxresp['time'] = core.get_station_and_time( wxdata) if 'CAVOK' not in wxdata: wxdata, wxresp['clouds'] = core.get_clouds(wxdata) wxdata, wxresp['wind_direction'], wxresp['wind_speed'], \ wxresp['wind_gust'], wxresp['wind_variable_direction'] = core.get_wind(wxdata, units) wxdata, wxresp['altimeter'] = core.get_altimeter(wxdata, units, 'IN') if 'CAVOK' in wxdata: wxresp['visibility'] = core.make_number('CAVOK') wxresp['clouds'] = [] wxdata.remove('CAVOK') else: wxdata, wxresp['visibility'] = core.get_visibility(wxdata, units) wxresp['other'], wxresp['temperature'], wxresp[ 'dewpoint'] = core.get_temp_and_dew(wxdata) condition = core.get_flight_rules(wxresp['visibility'], core.get_ceiling(wxresp['clouds'])) wxresp['flight_rules'] = FLIGHT_RULES[condition] wxresp['remarks_info'] = remarks.parse(wxresp['remarks']) wxresp['time'] = core.make_timestamp(wxresp['time']) return MetarData(**wxresp), units
def parse_lines(lines: [str], units: Units, use_na: bool = True) -> [dict]: """ Returns a list of parsed line dictionaries """ parsed_lines = [] prob = '' while lines: raw_line = lines[0].strip() line = core.sanitize_line(raw_line) # Remove prob from the beginning of a line if line.startswith('PROB'): # Add standalone prob to next line if len(line) == 6: prob = line line = '' # Add to current line elif len(line) > 6: prob = line[:6] line = line[6:].strip() if line: parsed_line = (parse_na_line if use_na else parse_in_line)(line, units) for key in ('start_time', 'end_time'): parsed_line[key] = core.make_timestamp(parsed_line[key]) parsed_line['probability'] = core.make_number(prob[4:]) parsed_line['raw'] = raw_line parsed_line['sanitized'] = prob + ' ' + line if prob else line prob = '' parsed_lines.append(parsed_line) lines.pop(0) return parsed_lines
def test_find_missing_taf_times(self): """ Tests that missing forecast times can be interpretted by """ good_lines = [{ 'type': 'FROM', 'start_time': '3021', 'end_time': '3023' }, { 'type': 'FROM', 'start_time': '3023', 'end_time': '0105' }, { 'type': 'FROM', 'start_time': '0105', 'end_time': '0108' }, { 'type': 'FROM', 'start_time': '0108', 'end_time': '0114' }] for line in good_lines: for key in ('start_time', 'end_time'): line[key] = core.make_timestamp(line[key]) bad_lines = deepcopy(good_lines) bad_lines[0]['start_time'] = None bad_lines[1]['start_time'] = None bad_lines[2]['end_time'] = None bad_lines[3]['end_time'] = None start, end = good_lines[0]['start_time'], good_lines[-1]['end_time'] self.assertEqual(core.find_missing_taf_times(bad_lines, start, end), good_lines)
def parse_na(report: str) -> (MetarData, Units): """ Parser for the North American METAR variant """ units = Units(**NA_UNITS) clean = core.sanitize_report_string(report) wxresp = {'raw': report, 'sanitized': clean} wxdata, wxresp['remarks'] = core.get_remarks(clean) wxdata = core.dedupe(wxdata) wxdata, wxresp['runway_visibility'], _ = core.sanitize_report_list(wxdata) wxdata, wxresp['station'], wxresp['time'] = core.get_station_and_time( wxdata) wxdata, wxresp['clouds'] = core.get_clouds(wxdata) wxdata, wxresp['wind_direction'], wxresp['wind_speed'], \ wxresp['wind_gust'], wxresp['wind_variable_direction'] = core.get_wind(wxdata, units) wxdata, wxresp['altimeter'] = core.get_altimeter(wxdata, units, 'NA') wxdata, wxresp['visibility'] = core.get_visibility(wxdata, units) wxresp['other'], wxresp['temperature'], wxresp[ 'dewpoint'] = core.get_temp_and_dew(wxdata) condition = core.get_flight_rules(wxresp['visibility'], core.get_ceiling(wxresp['clouds'])) wxresp['flight_rules'] = FLIGHT_RULES[condition] wxresp['remarks_info'] = remarks.parse(wxresp['remarks']) wxresp['time'] = core.make_timestamp(wxresp['time']) return MetarData(**wxresp), units
def test_make_timestamp(self): """ Tests that a report timestamp is converted into a Timestamp dataclass """ today = datetime.utcnow() rts = today.strftime(r'%d%HZ') date = core.make_timestamp(rts) self.assertIsInstance(date, structs.Timestamp) self.assertEqual(date.repr, rts) self.assertEqual(date.dt.day, today.day) self.assertEqual(date.dt.hour, today.hour)
def parse(station: str, report: str) -> TafData: """ Returns TafData and Units dataclasses with parsed data and their associated units """ if not report: return None, None core.valid_station(station) while len(report) > 3 and report[:4] in ('TAF ', 'AMD ', 'COR '): report = report[4:] _, station, time = core.get_station_and_time(report[:20].split()) retwx = { 'end_time': None, 'raw': report, 'remarks': None, 'start_time': None, 'station': station, 'time': core.make_timestamp(time) } report = report.replace(station, '') report = report.replace(time, '').strip() if core.uses_na_format(station): use_na = True units = Units(**NA_UNITS) else: use_na = False units = Units(**IN_UNITS) # Find and remove remarks report, retwx['remarks'] = core.get_taf_remarks(report) # Split and parse each line lines = core.split_taf(report) parsed_lines = parse_lines(lines, units, use_na) # Perform additional info extract and corrections if parsed_lines: parsed_lines[-1]['other'], retwx['max_temp'], retwx['min_temp'] \ = core.get_temp_min_and_max(parsed_lines[-1]['other']) if not (retwx['max_temp'] or retwx['min_temp']): parsed_lines[0]['other'], retwx['max_temp'], retwx['min_temp'] \ = core.get_temp_min_and_max(parsed_lines[0]['other']) # Set start and end times based on the first line start, end = parsed_lines[0]['start_time'], parsed_lines[0]['end_time'] parsed_lines[0]['end_time'] = None retwx['start_time'], retwx['end_time'] = start, end parsed_lines = core.find_missing_taf_times(parsed_lines, start, end) parsed_lines = core.get_taf_flight_rules(parsed_lines) # Extract Oceania-specific data if retwx['station'][0] == 'A': parsed_lines[-1]['other'], retwx['alts'], retwx['temps'] \ = core.get_oceania_temp_and_alt(parsed_lines[-1]['other']) # Convert to dataclass retwx['forecast'] = [TafLineData(**line) for line in parsed_lines] return TafData(**retwx), units
def test_type_and_times(self): """ Tests line start from type, time, and probability values """ for type, *times, prob, spoken in ( (None, None, None, None, ''), ('FROM', '2808', '2815', None, 'From 8 to 15 zulu,'), ('FROM', '2822', '2903', None, 'From 22 to 3 zulu,'), ('BECMG', '3010', None, None, 'At 10 zulu becoming'), ('PROB', '1303', '1305', '30', r"From 3 to 5 zulu, there's a 30% chance for"), ('INTER', '1303', '1305', '45', r"From 3 to 5 zulu, there's a 45% chance for intermittent"), ('INTER', '2423', '2500', None, 'From 23 to midnight zulu, intermittent'), ('TEMPO', '0102', '0103', None, 'From 2 to 3 zulu, temporary'), ): times = [core.make_timestamp(time) for time in times] if prob is not None: prob = core.make_number(prob) ret = speech.type_and_times(type, *times, prob) self.assertIsInstance(ret, str) self.assertEqual(ret, spoken)
def test_find_missing_taf_times(self): """ Tests that missing forecast times can be interpretted by """ good_lines = [ {'type': 'FROM', 'start_time': '3021', 'end_time': '3023'}, {'type': 'FROM', 'start_time': '3023', 'end_time': '0105'}, {'type': 'FROM', 'start_time': '0105', 'end_time': '0108'}, {'type': 'FROM', 'start_time': '0108', 'end_time': '0114'} ] for line in good_lines: for key in ('start_time', 'end_time'): line[key] = core.make_timestamp(line[key]) bad_lines = deepcopy(good_lines) bad_lines[0]['start_time'] = None bad_lines[1]['start_time'] = None bad_lines[2]['end_time'] = None bad_lines[3]['end_time'] = None start, end = good_lines[0]['start_time'], good_lines[-1]['end_time'] self.assertEqual(core.find_missing_taf_times(bad_lines, start, end), good_lines)
def test_taf(self): """ Tests converting a TafData report into a single spoken string """ units = structs.Units(**static.NA_UNITS) empty_line = { k: None for k in structs.TafLineData.__dataclass_fields__.keys() } forecast = [ structs.TafLineData(**{ **empty_line, **line }) for line in ( { 'type': 'FROM', 'start_time': core.make_timestamp('0410Z'), 'end_time': core.make_timestamp('0414Z'), 'visibility': core.make_number('3'), 'wind_direction': core.make_number('360'), 'wind_gust': core.make_number('20'), 'wind_speed': core.make_number('12'), }, { 'type': 'PROB', 'probability': core.make_number('45'), 'start_time': core.make_timestamp('0412Z'), 'end_time': core.make_timestamp('0414Z'), 'visibility': core.make_number('M1/4'), }, ) ] taf = structs.TafData(raw=None, remarks=None, station=None, time=None, forecast=forecast, start_time=core.make_timestamp('0410Z'), end_time=core.make_timestamp('0414Z')) ret = speech.taf(taf, units) spoken = ( f"Starting on {taf.start_time.dt.strftime('%B')} 4th - From 10 to 14 zulu, " "Winds three six zero at 12kt gusting to 20kt. Visibility three miles. " r"From 12 to 14 zulu, there's a 45% chance for Visibility " "less than one quarter of a mile") self.assertIsInstance(ret, str) self.assertEqual(ret, spoken)
def test_taf(self): """ Tests converting a TafData report into a single spoken string """ units = structs.Units(**static.NA_UNITS) empty_line = {k: None for k in structs.TafLineData.__dataclass_fields__.keys()} forecast = [structs.TafLineData(**{**empty_line, **line}) for line in ( { 'type': 'FROM', 'start_time': core.make_timestamp('0410Z'), 'end_time': core.make_timestamp('0414Z'), 'visibility': core.make_number('3'), 'wind_direction': core.make_number('360'), 'wind_gust': core.make_number('20'), 'wind_speed': core.make_number('12'), }, { 'type': 'PROB', 'probability': core.make_number('45'), 'start_time': core.make_timestamp('0412Z'), 'end_time': core.make_timestamp('0414Z'), 'visibility': core.make_number('M1/4'), }, )] taf = structs.TafData( raw=None, remarks=None, station=None, time=None, forecast=forecast, start_time=core.make_timestamp('0410Z'), end_time=core.make_timestamp('0414Z') ) ret = speech.taf(taf, units) spoken = (f"Starting on {taf.start_time.dt.strftime('%B')} 4th - From 10 to 14 zulu, " "Winds three six zero at 12kt gusting to 20kt. Visibility three miles. " r"From 12 to 14 zulu, there's a 45% chance for Visibility " "less than one quarter of a mile") self.assertIsInstance(ret, str) self.assertEqual(ret, spoken)
def _time(item: str) -> Timestamp: """ Convert a time element to a Timestamp """ return core.make_timestamp(item, time_only=True)