From ee5304ec8b3baef7013d91abffdee13cbf97e3b9 Mon Sep 17 00:00:00 2001 From: Jules Dejaeghere Date: Sun, 23 Jun 2024 14:42:10 +0200 Subject: [PATCH] Add tests for BelAQI and fix bugs --- src/open_irceline/api.py | 2 - src/open_irceline/belaqi.py | 38 ++- src/open_irceline/utils.py | 2 +- tests/conftest.py | 3 +- tests/fixtures/rio_wfs_for_belaqi.json | 319 +++++++++++++++++++++++++ tests/test_api_rio.py | 6 +- tests/test_belaqi.py | 58 ++++- 7 files changed, 407 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/rio_wfs_for_belaqi.json diff --git a/src/open_irceline/api.py b/src/open_irceline/api.py index 4ee3577..6e61157 100644 --- a/src/open_irceline/api.py +++ b/src/open_irceline/api.py @@ -40,7 +40,6 @@ async def _api_wrapper(self, url: str, querystring: dict = None, headers: dict = :param querystring: dict to build the query string :return: response from the client """ - if headers is None: headers = dict() if 'User-Agent' not in headers: @@ -228,7 +227,6 @@ async def get_data(self, except IrcelineApiError: # retry for the day before yesterday = timestamp - timedelta(days=1) - print('here') url = f"{forecast_base_url}/BE_{feature}_{yesterday.strftime('%Y%m%d')}_d{d}.csv" try: r: ClientResponse = await self._api_cached_wrapper(url) diff --git a/src/open_irceline/belaqi.py b/src/open_irceline/belaqi.py index 4366ae6..82b52c6 100644 --- a/src/open_irceline/belaqi.py +++ b/src/open_irceline/belaqi.py @@ -23,6 +23,8 @@ def belaqi_index(pm10: float, pm25: float, o3: float, no2: float) -> BelAqiIndex :param no2: NO2 max 1-hourly mean per day (or latest hourly mean for real-time) (µg/m³) :return: BelAQI index from 1 to 10 (Value of BelAqiIndex enum) """ + if pm10 is None or pm25 is None or o3 is None or no2 is None: + raise ValueError("All the components should be valued (at lest one is None here)") if pm10 < 0 or pm25 < 0 or o3 < 0 or no2 < 0: raise ValueError("All the components should have a positive value") @@ -62,6 +64,7 @@ async def belaqi_index_actual(rio_client: IrcelineRioClient, position: Tuple[flo timestamp: datetime | None = None) -> BelAqiIndex: """ Get current BelAQI index value for the given position using the rio_client + Raise ValueError if one or more components are not available :param rio_client: client for the RIO WFS service :param position: position for which to get the data :param timestamp: desired time for the data (now if None) @@ -71,21 +74,27 @@ async def belaqi_index_actual(rio_client: IrcelineRioClient, position: Tuple[flo timestamp = datetime.utcnow() components = await rio_client.get_data( timestamp=timestamp, - features=[RioFeature.PM10_24HMEAN, RioFeature.PM25_24HMEAN, RioFeature.O3_HMEAN, RioFeature.NO2_HMEAN], + features=[RioFeature.PM10_24HMEAN, + RioFeature.PM25_24HMEAN, + RioFeature.O3_HMEAN, + RioFeature.NO2_HMEAN], position=position ) - return belaqi_index(components[RioFeature.PM10_24HMEAN].get('value', -1), - components[RioFeature.PM25_24HMEAN].get('value', -1), - components[RioFeature.O3_HMEAN].get('value', -1), - components[RioFeature.NO2_HMEAN].get('value', -1)) + return belaqi_index( + components.get(RioFeature.PM10_24HMEAN, {}).get('value', -1), + components.get(RioFeature.PM25_24HMEAN, {}).get('value', -1), + components.get(RioFeature.O3_HMEAN, {}).get('value', -1), + components.get(RioFeature.NO2_HMEAN, {}).get('value', -1) + ) async def belaqi_index_forecast(forecast_client: IrcelineForecastClient, position: Tuple[float, float], - timestamp: date | None = None) -> Dict[date, BelAqiIndex]: + timestamp: date | None = None) -> Dict[date, BelAqiIndex | None]: """ Get forecasted BelAQI index value for the given position using the forecast_client. Data is downloaded for the given day and the four next days + Value is None for the date if one or more components cannot be downloaded :param forecast_client: client for the forecast data :param position: position for which to get the data :param timestamp: day at which the forecast are issued @@ -95,7 +104,9 @@ async def belaqi_index_forecast(forecast_client: IrcelineForecastClient, positio timestamp = date.today() components = await forecast_client.get_data( timestamp=timestamp, - features=[ForecastFeature.PM10_DMEAN, ForecastFeature.PM25_DMEAN, ForecastFeature.O3_MAXHMEAN, + features=[ForecastFeature.PM10_DMEAN, + ForecastFeature.PM25_DMEAN, + ForecastFeature.O3_MAXHMEAN, ForecastFeature.NO2_MAXHMEAN], position=position ) @@ -103,9 +114,14 @@ async def belaqi_index_forecast(forecast_client: IrcelineForecastClient, positio result = dict() for _, day in components.keys(): - result[day] = belaqi_index(components[(ForecastFeature.PM10_DMEAN, day)].get('value', -1), - components[(ForecastFeature.PM25_DMEAN, day)].get('value', -1), - components[(ForecastFeature.O3_MAXHMEAN, day)].get('value', -1), - components[(ForecastFeature.NO2_MAXHMEAN, day)].get('value', -1)) + try: + result[day] = belaqi_index( + components.get((ForecastFeature.PM10_DMEAN, day), {}).get('value', -1), + components.get((ForecastFeature.PM25_DMEAN, day), {}).get('value', -1), + components.get((ForecastFeature.O3_MAXHMEAN, day), {}).get('value', -1), + components.get((ForecastFeature.NO2_MAXHMEAN, day), {}).get('value', -1) + ) + except ValueError: + result[day] = None return result diff --git a/src/open_irceline/utils.py b/src/open_irceline/utils.py index 65480a6..1e8a29b 100644 --- a/src/open_irceline/utils.py +++ b/src/open_irceline/utils.py @@ -15,7 +15,7 @@ def __setitem__(self, key, value): super().__setitem__(key, value) self.move_to_end(key) if len(self) > self._size: - print('drop', self.popitem(False)[0]) + self.popitem(False) def __getitem__(self, key): self.move_to_end(key) diff --git a/tests/conftest.py b/tests/conftest.py index 626601d..0689299 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ import aiohttp - def get_api_data(fixture: str, plain=False) -> str | dict: with open(f'tests/fixtures/{fixture}', 'r') as file: if plain: @@ -12,7 +11,7 @@ def get_api_data(fixture: str, plain=False) -> str | dict: return json.load(file) -def get_mock_session_json(json_file=None, text_file=None): +def get_mock_session(json_file=None, text_file=None): # Create the mock response mock_response = Mock() if json_file is not None: diff --git a/tests/fixtures/rio_wfs_for_belaqi.json b/tests/fixtures/rio_wfs_for_belaqi.json new file mode 100644 index 0000000..bebef86 --- /dev/null +++ b/tests/fixtures/rio_wfs_for_belaqi.json @@ -0,0 +1,319 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "pm10_24hmean.fid--d1ce43_19045107e20_1893", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T11:00:00Z", + "value": 7.3, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "pm10_24hmean.fid--d1ce43_19045107e20_1894", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T12:00:00Z", + "value": 7.3, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "pm25_24hmean.fid--d1ce43_19045107e20_1895", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T11:00:00Z", + "value": 3.3, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "pm25_24hmean.fid--d1ce43_19045107e20_1896", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T12:00:00Z", + "value": 3.2, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "o3_hmean.fid--d1ce43_19045107e20_1897", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T11:00:00Z", + "value": 69, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "o3_hmean.fid--d1ce43_19045107e20_1898", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T12:00:00Z", + "value": 73, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "no2_hmean.fid--d1ce43_19045107e20_1899", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T11:00:00Z", + "value": 4, + "network": "Wallonia" + } + }, + { + "type": "Feature", + "id": "no2_hmean.fid--d1ce43_19045107e20_189a", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 182000, + 128000 + ], + [ + 182000, + 132000 + ], + [ + 186000, + 132000 + ], + [ + 186000, + 128000 + ], + [ + 182000, + 128000 + ] + ] + ] + }, + "geometry_name": "the_geom", + "properties": { + "id": 1102, + "timestamp": "2024-06-23T12:00:00Z", + "value": 3, + "network": "Wallonia" + } + } + ], + "totalFeatures": 8, + "numberMatched": 8, + "numberReturned": 8, + "timeStamp": "2024-06-23T12:31:50.858Z", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::31370" + } + } +} \ No newline at end of file diff --git a/tests/test_api_rio.py b/tests/test_api_rio.py index 68a719e..ae9b228 100644 --- a/tests/test_api_rio.py +++ b/tests/test_api_rio.py @@ -6,7 +6,7 @@ from src.open_irceline.api import IrcelineRioClient from src.open_irceline.data import RioFeature, FeatureValue from src.open_irceline.utils import epsg_transform -from tests.conftest import get_api_data, get_mock_session_json +from tests.conftest import get_api_data, get_mock_session @freeze_time(datetime.fromisoformat("2024-06-15T16:55:03.419Z")) @@ -93,7 +93,7 @@ def test_parse_capabilities_with_error(): async def test_api_rio(): pos = (50.4657, 4.8647) x, y = epsg_transform(pos) - session = get_mock_session_json('rio_wfs.json') + session = get_mock_session('rio_wfs.json') client = IrcelineRioClient(session) @@ -117,7 +117,7 @@ async def test_api_rio(): async def test_api_rio_get_capabilities(): - session = get_mock_session_json(text_file='capabilities.xml') + session = get_mock_session(text_file='capabilities.xml') client = IrcelineRioClient(session) _ = await client.get_rio_capabilities() diff --git a/tests/test_belaqi.py b/tests/test_belaqi.py index 3d4ffde..85ea790 100644 --- a/tests/test_belaqi.py +++ b/tests/test_belaqi.py @@ -1,7 +1,12 @@ +from datetime import date, timedelta, datetime from random import randint, seed +from freezegun import freeze_time import pytest -from src.open_irceline.belaqi import belaqi_index + +from src.open_irceline.api import IrcelineForecastClient, IrcelineRioClient +from src.open_irceline.belaqi import belaqi_index, belaqi_index_forecast, belaqi_index_actual from src.open_irceline.data import BelAqiIndex +from tests.conftest import get_mock_session_many_csv, get_mock_session def test_belaqi_index(): @@ -140,4 +145,53 @@ def test_belaqi_value_error(): with pytest.raises(ValueError): belaqi_index(1, 0, 12, -8888) -# TODO add more test for the other BelAQI functions \ No newline at end of file + + +@freeze_time(datetime.fromisoformat("2024-06-19T19:30:09.581Z")) +async def test_belaqi_index_forecast(): + session = get_mock_session_many_csv() + client = IrcelineForecastClient(session) + pos = (50.55, 4.85) + + result = await belaqi_index_forecast(client, pos) + + expected_days = {date(2024, 6, 19) + timedelta(days=i) for i in range(5)} + + assert set(result.keys()) == expected_days + for v in result.values(): + assert v == BelAqiIndex.GOOD + + +async def test_belaqi_index_forecast_missing_day(): + session = get_mock_session_many_csv() + client = IrcelineForecastClient(session) + pos = (50.55, 4.85) + + result = await belaqi_index_forecast(client, pos, date(2024, 6, 21)) + + expected_days = {date(2024, 6, 21) + timedelta(days=i) for i in range(5)} + print(result) + assert set(result.keys()) == expected_days + for v in result.values(): + assert v is None + + +@freeze_time(datetime.fromisoformat("2024-06-23T12:30:09.581Z")) +async def test_belaqi_index_actual(): + session = get_mock_session(json_file='rio_wfs_for_belaqi.json') + client = IrcelineRioClient(session) + pos = (50.55, 4.85) + + result = await belaqi_index_actual(client, pos) + print(result) + assert result == BelAqiIndex.FAIRLY_GOOD + + +@freeze_time(datetime.fromisoformat("2024-06-23T12:30:09.581Z")) +async def test_belaqi_index_actual_missing_value(): + session = get_mock_session(json_file='rio_wfs.json') + client = IrcelineRioClient(session) + pos = (50.55, 4.85) + + with pytest.raises(ValueError): + _ = await belaqi_index_actual(client, pos)