def test_taf_line(self): """Tests converting TAF line data into into a single spoken string""" units = structs.Units(**static.core.NA_UNITS) line = { "altimeter": parse_altimeter("2992"), "clouds": [core.make_cloud("BKN015CB")], "end_time": core.make_timestamp("1206"), "icing": ["611005"], "other": [], "start_time": core.make_timestamp("1202"), "transition_start": None, "turbulence": ["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"), "wx_codes": get_wx_codes(["+RA"])[1], } 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 2000ft 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_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 parse_na(report: str) -> (MetarData, Units): """ Parser for the North American METAR variant """ units = Units(**NA_UNITS) wxresp = {"raw": report} clean = core.sanitize_report_string(report) wxdata, wxresp["remarks"] = get_remarks(clean) wxdata = core.dedupe(wxdata) wxdata = core.sanitize_report_list(wxdata) wxresp["sanitized"] = " ".join(wxdata + [wxresp["remarks"]]) wxdata, wxresp["station"], wxresp["time"] = core.get_station_and_time( wxdata) wxdata, wxresp["runway_visibility"] = get_runway_visibility(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"] = get_altimeter(wxdata, units, "NA") wxdata, wxresp["visibility"] = core.get_visibility(wxdata, units) wxdata, wxresp["temperature"], wxresp["dewpoint"] = get_temp_and_dew( wxdata) condition = core.get_flight_rules(wxresp["visibility"], core.get_ceiling(wxresp["clouds"])) wxresp["other"], wxresp["wx_codes"] = get_wx_codes(wxdata) 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 = 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 if prob: parsed_line[ "sanitized"] = prob + " " + parsed_line["sanitized"] prob = "" parsed_lines.append(parsed_line) lines.pop(0) return parsed_lines
def parse_in(report: str, issued: date = None) -> (MetarData, Units): """ Parser for the International METAR variant """ units = Units(**IN_UNITS) resp = {"raw": report} resp["sanitized"], resp["remarks"], data = sanitize(report) data, resp["station"], resp["time"] = core.get_station_and_time(data) data, resp["runway_visibility"] = get_runway_visibility(data) if "CAVOK" not in data: data, resp["clouds"] = core.get_clouds(data) ( data, resp["wind_direction"], resp["wind_speed"], resp["wind_gust"], resp["wind_variable_direction"], ) = core.get_wind(data, units) data, resp["altimeter"] = get_altimeter(data, units, "IN") if "CAVOK" in data: resp["visibility"] = core.make_number("CAVOK") resp["clouds"] = [] data.remove("CAVOK") else: data, resp["visibility"] = core.get_visibility(data, units) data, resp["temperature"], resp["dewpoint"] = get_temp_and_dew(data) condition = core.get_flight_rules(resp["visibility"], core.get_ceiling(resp["clouds"])) resp["other"], resp["wx_codes"] = get_wx_codes(data) resp["flight_rules"] = FLIGHT_RULES[condition] resp["remarks_info"] = remarks.parse(resp["remarks"]) resp["time"] = core.make_timestamp(resp["time"], target_date=issued) return MetarData(**resp), units
def parse_na(report: str, issued: date = None) -> Tuple[MetarData, Units]: """Parser for the North American METAR variant""" units = Units(**NA_UNITS) resp = {"raw": report} resp["sanitized"], resp["remarks"], data = sanitize(report) data, resp["station"], resp["time"] = core.get_station_and_time(data) data, resp["runway_visibility"] = get_runway_visibility(data) data, resp["clouds"] = core.get_clouds(data) ( data, resp["wind_direction"], resp["wind_speed"], resp["wind_gust"], resp["wind_variable_direction"], ) = core.get_wind(data, units) data, resp["altimeter"] = get_altimeter(data, units, "NA") data, resp["visibility"] = core.get_visibility(data, units) data, resp["temperature"], resp["dewpoint"] = get_temp_and_dew(data) condition = core.get_flight_rules( resp["visibility"], core.get_ceiling(resp["clouds"]) ) resp["other"], resp["wx_codes"] = get_wx_codes(data) resp["flight_rules"] = FLIGHT_RULES[condition] resp["remarks_info"] = remarks.parse(resp["remarks"]) resp["time"] = core.make_timestamp(resp["time"], target_date=issued) return MetarData(**resp), units
def test_make_timestamp(self): """ Tests that a report timestamp is converted into a Timestamp dataclass """ today = datetime.now(tz=timezone.utc) 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 test_make_timestamp(self): """Tests that a report timestamp is converted into a Timestamp dataclass""" for dt, fmt, target in ( (datetime.now(tz=timezone.utc), r"%d%HZ", False), (datetime.now(tz=timezone.utc), r"%d%H%MZ", False), (datetime(2010, 2, 2, 2, 2, tzinfo=timezone.utc), r"%d%HZ", True), (datetime(2010, 2, 2, 2, 2, tzinfo=timezone.utc), r"%d%H%MZ", True), ): dt_repr = dt.strftime(fmt) target = dt.date() if target else None dt = dt.replace(second=0, microsecond=0) if "%M" not in fmt: dt = dt.replace(minute=0) ts = core.make_timestamp(dt_repr, target_date=target) self.assert_timestamp(ts, dt_repr, dt)
def test_find_missing_taf_times(self): """ Tests that missing forecast times can be interpretted by """ good_lines = [ { "type": "FROM", "transition_start": None, "start_time": "3021", "end_time": "3023", }, { "type": "FROM", "transition_start": None, "start_time": "3023", "end_time": "0105", }, { "type": "BECMG", "transition_start": "0105", "start_time": "0107", "end_time": "0108", }, { "type": "FROM", "transition_start": None, "start_time": "0108", "end_time": "0114", }, ] for line in good_lines: for key in ("start_time", "end_time", "transition_start"): 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[1]["end_time"] = None bad_lines[2]["end_time"] = None # This None implies normal parsing bad_lines[3]["end_time"] = None start, end = good_lines[0]["start_time"], good_lines[-1]["end_time"] self.assertEqual(taf.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.core.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 parse(station: str, report: str) -> (TafData, Units): """ Returns TafData and Units dataclasses with parsed data and their associated units """ if not report: return None, None valid_station(station) while len(report) > 3 and report[:4] in ("TAF ", "AMD ", "COR "): report = report[4:] retwx = { "end_time": None, "raw": report, "remarks": None, "start_time": None } report = core.sanitize_report_string(report) _, station, time = core.get_station_and_time(report[:20].split()) retwx["station"] = station retwx["time"] = core.make_timestamp(time) report = report.replace(station, "") if time: report = report.replace(time, "").strip() if 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"] = get_taf_remarks(report) # Split and parse each line lines = 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"], ) = 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"], ) = 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 = find_missing_taf_times(parsed_lines, start, end) parsed_lines = get_taf_flight_rules(parsed_lines) # Extract Oceania-specific data if retwx["station"][0] == "A": ( parsed_lines[-1]["other"], retwx["alts"], retwx["temps"], ) = get_oceania_temp_and_alt(parsed_lines[-1]["other"]) # Convert wx codes for i, line in enumerate(parsed_lines): parsed_lines[i]["other"], parsed_lines[i]["wx_codes"] = get_wx_codes( line["other"]) # Convert to dataclass retwx["forecast"] = [TafLineData(**line) for line in parsed_lines] return TafData(**retwx), units
def _time(item: str, target: date = None) -> Timestamp: """ Convert a time element to a Timestamp """ return core.make_timestamp(item, time_only=True, target_date=target)
def _time(item: str) -> Timestamp: """ Convert a time element to a Timestamp """ return core.make_timestamp(item, time_only=True)