diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index ffab177..5d34e8a 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -38,11 +38,12 @@ request some data. To do this, use the `manager` object: :: - forecast = manager.get_forecast(51, 0, "hourly") + forecast = manager.get_forecast(51, 0, "hourly", convert_weather_code=True) -This takes three parameters: the latitude and longitude of the location you want -a forecast for and also a forecast type of “hourly”. We’ll discuss the forecast -types later on. +This takes four parameters: the latitude and longitude of the location you want +a forecast for, a forecast type of “hourly” and an instruction to convert the +numeric weather code to a string description. We’ll discuss the forecast types +later on. This Forecast Object which has been returned to us contains lots of information which we will cover in a later section, right now we’re just going to get the diff --git a/src/datapoint/Forecast.py b/src/datapoint/Forecast.py index 1c894de..0bcb463 100644 --- a/src/datapoint/Forecast.py +++ b/src/datapoint/Forecast.py @@ -16,7 +16,12 @@ class Forecast: >>> import datapoint >>> m = datapoint.Manager.Manager(api_key = "blah") - >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly") + >>> f = m.get_forecast( + latitude=50, + longitude=0, + frequency="hourly", + convert_weather_code=True, + ) >>> f.now() { 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc), @@ -101,12 +106,14 @@ class Forecast: } """ - def __init__(self, frequency, api_data): + def __init__(self, frequency, api_data, convert_weather_code): """ :param frequency: Frequency of forecast: 'hourly', 'three-hourly' or 'daily' :param api_data: Data returned from API call + :param: convert_weather_code: Convert numeric weather codes to string description :type frequency: string :type api_data: dict + :type convert_weather_code: bool """ self.frequency = frequency # Need to parse format like 2024-02-17T15:00Z. This can only be @@ -136,6 +143,10 @@ def __init__(self, frequency, api_data): 2 ] #: The elevation of the location of the provided forecast + self.convert_weather_code = ( + convert_weather_code #: Convert numeric weather codes to string description + ) + forecasts = api_data["features"][0]["properties"]["timeSeries"] parameters = api_data["parameters"][0] if frequency == "daily": @@ -203,23 +214,57 @@ def _build_timesteps_from_daily(self, forecasts, parameters): case_corrected_element = ( trimmed_element[0].lower() + trimmed_element[1:] ) - day_step[case_corrected_element] = { - "value": value, - "description": parameters[element]["description"], - "unit_name": parameters[element]["unit"]["label"], - "unit_symbol": parameters[element]["unit"]["symbol"]["type"], - } + + if ( + case_corrected_element == "significantWeatherCode" + and self.convert_weather_code + ): + day_step[case_corrected_element] = { + "value": WEATHER_CODES[str(value)], + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"][ + "type" + ], + } + + else: + day_step[case_corrected_element] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"][ + "type" + ], + } elif element.startswith("night"): trimmed_element = element.replace("night", "") case_corrected_element = ( trimmed_element[0].lower() + trimmed_element[1:] ) - night_step[case_corrected_element] = { - "value": value, - "description": parameters[element]["description"], - "unit_name": parameters[element]["unit"]["label"], - "unit_symbol": parameters[element]["unit"]["symbol"]["type"], - } + + if ( + case_corrected_element == "significantWeatherCode" + and self.convert_weather_code + ): + night_step[case_corrected_element] = { + "value": WEATHER_CODES[str(value)], + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"][ + "type" + ], + } + + else: + night_step[case_corrected_element] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"][ + "type" + ], + } elif element == "maxUvIndex": day_step[element] = { "value": value, @@ -260,7 +305,7 @@ def _build_timestep(self, forecast, parameters): forecast["time"], "%Y-%m-%dT%H:%M%z" ) - elif element == "significantWeatherCode": + elif element == "significantWeatherCode" and self.convert_weather_code: timestep[element] = { "value": WEATHER_CODES[str(value)], "description": parameters[element]["description"], diff --git a/src/datapoint/Manager.py b/src/datapoint/Manager.py index b2f01de..68c7556 100644 --- a/src/datapoint/Manager.py +++ b/src/datapoint/Manager.py @@ -18,7 +18,12 @@ class Manager: >>> import datapoint >>> m = datapoint.Manager.Manager(api_key = "blah") - >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly") + >>> f = m.get_forecast( + latitude=50, + longitude=0, + frequency="hourly", + convert_weather_code=True + ) >>> f.now() { 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc), @@ -201,16 +206,20 @@ def __call_api(self, latitude, longitude, frequency): return data - def get_forecast(self, latitude, longitude, frequency="daily"): + def get_forecast( + self, latitude, longitude, frequency="daily", convert_weather_code=True + ): """ Get a forecast for the provided site :parameter latitude: Latitude of forecast location :parameter longitude: Longitude of forecast location :parameter frequency: Forecast frequency. One of 'hourly', 'three-hourly, 'daily' + :parameter convert_weather_code: Convert numeric weather codes to string description :type latitude: float :type longitude: float :type frequency: string + :type convert_weather_code: bool :return: :class: `Forecast ` object :rtype: datapoint.Forecast @@ -220,6 +229,10 @@ def get_forecast(self, latitude, longitude, frequency="daily"): "frequency must be set to one of 'hourly', 'three-hourly', 'daily'" ) data = self.__call_api(latitude, longitude, frequency) - forecast = Forecast(frequency=frequency, api_data=data) + forecast = Forecast( + frequency=frequency, + api_data=data, + convert_weather_code=convert_weather_code, + ) return forecast diff --git a/src/datapoint/weather_codes.py b/src/datapoint/weather_codes.py index d54cce4..23168fb 100644 --- a/src/datapoint/weather_codes.py +++ b/src/datapoint/weather_codes.py @@ -1,6 +1,6 @@ -# See https://www.metoffice.gov.uk/services/data/datapoint/code-definitions for definitions +# See https://datahub.metoffice.gov.uk/support/faqs for definitions WEATHER_CODES = { - "-1": "Trace rain", + "NA": "Not available", "0": "Clear night", "1": "Sunny day", "2": "Partly cloudy", diff --git a/tests/integration/test_manager.py b/tests/integration/test_manager.py index 38d83a5..625d7cf 100644 --- a/tests/integration/test_manager.py +++ b/tests/integration/test_manager.py @@ -28,7 +28,7 @@ def mock_get(*args, **kwargs): @pytest.fixture def hourly_forecast(mock_response_hourly): m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa") - f = m.get_forecast(50.9992, 0.0154, frequency="hourly") + f = m.get_forecast(50.9992, 0.0154, frequency="hourly", convert_weather_code=True) return f @@ -60,7 +60,9 @@ def mock_get(*args, **kwargs): @pytest.fixture def three_hourly_forecast(mock_response_three_hourly): m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa") - f = m.get_forecast(50.9992, 0.0154, frequency="three-hourly") + f = m.get_forecast( + 50.9992, 0.0154, frequency="three-hourly", convert_weather_code=True + ) return f @@ -92,7 +94,7 @@ def mock_get(*args, **kwargs): @pytest.fixture def daily_forecast(mock_response_daily): m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa") - f = m.get_forecast(50.9992, 0.0154, frequency="daily") + f = m.get_forecast(50.9992, 0.0154, frequency="daily", convert_weather_code=True) return f diff --git a/tests/reference_data/reference_data_test_forecast.py b/tests/reference_data/reference_data_test_forecast.py index 4e828a7..d30eed6 100644 --- a/tests/reference_data/reference_data_test_forecast.py +++ b/tests/reference_data/reference_data_test_forecast.py @@ -112,6 +112,118 @@ }, } +EXPECTED_FIRST_HOURLY_TIMESTEP_RAW_WEATHER_CODE = { + "time": datetime.datetime(2024, 2, 15, 19, 0, tzinfo=datetime.timezone.utc), + "screenTemperature": { + "value": 11.0, + "description": "Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "maxScreenAirTemp": { + "value": 11.55, + "description": "Maximum Screen Air Temperature Over Previous Hour", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minScreenAirTemp": { + "value": 10.98, + "description": "Minimum Screen Air Temperature Over Previous Hour", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "screenDewPointTemperature": { + "value": 8.94, + "description": "Screen Dew Point Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "feelsLikeTemperature": { + "value": 10.87, + "description": "Feels Like Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "windSpeed10m": { + "value": 1.18, + "description": "10m Wind Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 180, + "description": "10m Wind From Direction", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 6.69, + "description": "10m Wind Gust Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "max10mWindGust": { + "value": 8.92, + "description": "Maximum 10m Wind Gust Speed Over Previous Hour", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 19174, + "description": "Visibility", + "unit_name": "metres", + "unit_symbol": "m", + }, + "screenRelativeHumidity": { + "value": 86.99, + "description": "Screen Relative Humidity", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "mslp": { + "value": 100660, + "description": "Mean Sea Level Pressure", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "uvIndex": { + "value": 0, + "description": "UV Index", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "significantWeatherCode": { + "value": 2, + "description": "Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "precipitationRate": { + "value": 0.0, + "description": "Precipitation Rate", + "unit_name": "millimetres per hour", + "unit_symbol": "mm/h", + }, + "totalPrecipAmount": { + "value": 0.0, + "description": "Total Precipitation Amount Over Previous Hour", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "totalSnowAmount": { + "value": 0, + "description": "Total Snow Amount Over Previous Hour", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "probOfPrecipitation": { + "value": 4, + "description": "Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + EXPECTED_AT_DATETIME_HOURLY_TIMESTEP = { "time": datetime.datetime(2024, 2, 16, 19, 0, tzinfo=datetime.timezone.utc), "screenTemperature": { @@ -306,6 +418,130 @@ }, } EXPECTED_FIRST_DAILY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 16, 0, 0, tzinfo=datetime.timezone.utc), + "10MWindSpeed": { + "value": 1.39, + "description": "10m Wind Speed at Local Midnight", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "10MWindDirection": { + "value": 243, + "description": "10m Wind Direction at Local Midnight", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "10MWindGust": { + "value": 7.2, + "description": "10m Wind Gust Speed at Local Midnight", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 27712, + "description": "Visibility at Local Midnight", + "unit_name": "metres", + "unit_symbol": "m", + }, + "relativeHumidity": { + "value": 80.91, + "description": "Relative Humidity at Local Midnight", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "mslp": { + "value": 102640, + "description": "Mean Sea Level Pressure at Local Midnight", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "significantWeatherCode": { + "value": "Cloudy", + "description": "Night Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "minScreenTemperature": { + "value": 5.32, + "description": "Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "upperBoundMinTemp": { + "value": 9.17, + "description": "Upper Bound on Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "lowerBoundMinTemp": { + "value": 3.56, + "description": "Lower Bound on Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minFeelsLikeTemp": { + "value": 6.27, + "description": "Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "upperBoundMinFeelsLikeTemp": { + "value": 8.74, + "description": "Upper Bound on Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "lowerBoundMinFeelsLikeTemp": { + "value": 2.75, + "description": "Lower Bound on Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "probabilityOfPrecipitation": { + "value": 11, + "description": "Probability of Precipitation During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probabilityOfSnow": { + "value": 0, + "description": "Probability of Snow During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probabilityOfHeavySnow": { + "value": 0, + "description": "Probability of Heavy Snow During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probabilityOfRain": { + "value": 10, + "description": "Probability of Rain During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probabilityOfHeavyRain": { + "value": 0, + "description": "Probability of Heavy Rain During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probabilityOfHail": { + "value": 0, + "description": "Probability of Hail During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probabilityOfSferics": { + "value": 0, + "description": "Probability of Sferics During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_FIRST_DAILY_TIMESTEP_RAW_WEATHER_CODE = { "time": datetime.datetime(2024, 2, 16, 0, 0, tzinfo=datetime.timezone.utc), "10MWindSpeed": { "value": 1.39, @@ -468,7 +704,7 @@ "unit_symbol": "Pa", }, "significantWeatherCode": { - "value": 15, + "value": "Heavy rain", "description": "Night Significant Weather Code", "unit_name": "dimensionless", "unit_symbol": "1", @@ -597,7 +833,7 @@ "unit_symbol": "1", }, "significantWeatherCode": { - "value": 10, + "value": "Light rain shower", "description": "Day Significant Weather Code", "unit_name": "dimensionless", "unit_symbol": "1", diff --git a/tests/unit/test_forecast.py b/tests/unit/test_forecast.py index 2a498b1..40c71a2 100644 --- a/tests/unit/test_forecast.py +++ b/tests/unit/test_forecast.py @@ -33,17 +33,29 @@ def load_three_hourly_json(): @pytest.fixture def daily_forecast(load_daily_json): - return Forecast.Forecast("daily", load_daily_json) + return Forecast.Forecast("daily", load_daily_json, convert_weather_code=True) + + +@pytest.fixture +def daily_forecast_raw_weather_code(load_daily_json): + return Forecast.Forecast("daily", load_daily_json, convert_weather_code=False) @pytest.fixture def hourly_forecast(load_hourly_json): - return Forecast.Forecast("hourly", load_hourly_json) + return Forecast.Forecast("hourly", load_hourly_json, convert_weather_code=True) + + +@pytest.fixture +def hourly_forecast_raw_weather_code(load_hourly_json): + return Forecast.Forecast("hourly", load_hourly_json, convert_weather_code=False) @pytest.fixture def three_hourly_forecast(load_three_hourly_json): - return Forecast.Forecast("three-hourly", load_three_hourly_json) + return Forecast.Forecast( + "three-hourly", load_three_hourly_json, convert_weather_code=True + ) @pytest.fixture @@ -65,6 +77,11 @@ def expected_first_hourly_timestep(): return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP +@pytest.fixture +def expected_first_hourly_timestep_raw_weather_code(): + return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP_RAW_WEATHER_CODE + + @pytest.fixture def expected_at_datetime_hourly_timestep(): return reference_data_test_forecast.EXPECTED_AT_DATETIME_HOURLY_TIMESTEP @@ -80,6 +97,11 @@ def expected_first_daily_timestep(): return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP +@pytest.fixture +def expected_first_daily_timestep_raw_weather_code(): + return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP_RAW_WEATHER_CODE + + @pytest.fixture def expected_at_datetime_daily_timestep(): return reference_data_test_forecast.EXPECTED_AT_DATETIME_DAILY_TIMESTEP @@ -160,6 +182,16 @@ def test_requested_time_too_late(self, hourly_forecast): with pytest.raises(APIException): hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 19, 35)) + def test_forecast_first_timestep_raw_weather_code( + self, + hourly_forecast_raw_weather_code, + expected_first_hourly_timestep_raw_weather_code, + ): + assert ( + hourly_forecast_raw_weather_code.timesteps[0] + == expected_first_hourly_timestep_raw_weather_code + ) + class TestDailyForecast: def test_forecast_frequency(self, daily_forecast): @@ -212,6 +244,16 @@ def test_requested_time_too_late(self, daily_forecast): with pytest.raises(APIException): daily_forecast.at_datetime(datetime.datetime(2024, 2, 23, 19)) + def test_forecast_fist_timestep__raw_weather_code( + self, + daily_forecast_raw_weather_code, + expected_first_daily_timestep_raw_weather_code, + ): + assert ( + daily_forecast_raw_weather_code.timesteps[0] + == expected_first_daily_timestep_raw_weather_code + ) + class TestThreeHourlyForecast: def test_forecast_frequency(self, three_hourly_forecast):