Add tests for BelAQI and fix bugs

This commit is contained in:
Jules 2024-06-23 14:42:10 +02:00
parent 95fa6dde65
commit ee5304ec8b
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
7 changed files with 407 additions and 21 deletions

View file

@ -40,7 +40,6 @@ class IrcelineBaseClient(ABC):
:param querystring: dict to build the query string :param querystring: dict to build the query string
:return: response from the client :return: response from the client
""" """
if headers is None: if headers is None:
headers = dict() headers = dict()
if 'User-Agent' not in headers: if 'User-Agent' not in headers:
@ -228,7 +227,6 @@ class IrcelineForecastClient(IrcelineBaseClient):
except IrcelineApiError: except IrcelineApiError:
# retry for the day before # retry for the day before
yesterday = timestamp - timedelta(days=1) yesterday = timestamp - timedelta(days=1)
print('here')
url = f"{forecast_base_url}/BE_{feature}_{yesterday.strftime('%Y%m%d')}_d{d}.csv" url = f"{forecast_base_url}/BE_{feature}_{yesterday.strftime('%Y%m%d')}_d{d}.csv"
try: try:
r: ClientResponse = await self._api_cached_wrapper(url) r: ClientResponse = await self._api_cached_wrapper(url)

View file

@ -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/) :param no2: NO2 max 1-hourly mean per day (or latest hourly mean for real-time) (µg/)
:return: BelAQI index from 1 to 10 (Value of BelAqiIndex enum) :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: if pm10 < 0 or pm25 < 0 or o3 < 0 or no2 < 0:
raise ValueError("All the components should have a positive value") 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: timestamp: datetime | None = None) -> BelAqiIndex:
""" """
Get current BelAQI index value for the given position using the rio_client 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 rio_client: client for the RIO WFS service
:param position: position for which to get the data :param position: position for which to get the data
:param timestamp: desired time for the data (now if None) :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() timestamp = datetime.utcnow()
components = await rio_client.get_data( components = await rio_client.get_data(
timestamp=timestamp, 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 position=position
) )
return belaqi_index(components[RioFeature.PM10_24HMEAN].get('value', -1), return belaqi_index(
components[RioFeature.PM25_24HMEAN].get('value', -1), components.get(RioFeature.PM10_24HMEAN, {}).get('value', -1),
components[RioFeature.O3_HMEAN].get('value', -1), components.get(RioFeature.PM25_24HMEAN, {}).get('value', -1),
components[RioFeature.NO2_HMEAN].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], 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. 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 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 forecast_client: client for the forecast data
:param position: position for which to get the data :param position: position for which to get the data
:param timestamp: day at which the forecast are issued :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() timestamp = date.today()
components = await forecast_client.get_data( components = await forecast_client.get_data(
timestamp=timestamp, 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], ForecastFeature.NO2_MAXHMEAN],
position=position position=position
) )
@ -103,9 +114,14 @@ async def belaqi_index_forecast(forecast_client: IrcelineForecastClient, positio
result = dict() result = dict()
for _, day in components.keys(): for _, day in components.keys():
result[day] = belaqi_index(components[(ForecastFeature.PM10_DMEAN, day)].get('value', -1), try:
components[(ForecastFeature.PM25_DMEAN, day)].get('value', -1), result[day] = belaqi_index(
components[(ForecastFeature.O3_MAXHMEAN, day)].get('value', -1), components.get((ForecastFeature.PM10_DMEAN, day), {}).get('value', -1),
components[(ForecastFeature.NO2_MAXHMEAN, 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 return result

View file

@ -15,7 +15,7 @@ class SizedDict(OrderedDict):
super().__setitem__(key, value) super().__setitem__(key, value)
self.move_to_end(key) self.move_to_end(key)
if len(self) > self._size: if len(self) > self._size:
print('drop', self.popitem(False)[0]) self.popitem(False)
def __getitem__(self, key): def __getitem__(self, key):
self.move_to_end(key) self.move_to_end(key)

View file

@ -4,7 +4,6 @@ from unittest.mock import Mock, AsyncMock
import aiohttp import aiohttp
def get_api_data(fixture: str, plain=False) -> str | dict: def get_api_data(fixture: str, plain=False) -> str | dict:
with open(f'tests/fixtures/{fixture}', 'r') as file: with open(f'tests/fixtures/{fixture}', 'r') as file:
if plain: if plain:
@ -12,7 +11,7 @@ def get_api_data(fixture: str, plain=False) -> str | dict:
return json.load(file) 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 # Create the mock response
mock_response = Mock() mock_response = Mock()
if json_file is not None: if json_file is not None:

319
tests/fixtures/rio_wfs_for_belaqi.json vendored Normal file
View file

@ -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"
}
}
}

View file

@ -6,7 +6,7 @@ from src.open_irceline import rio_wfs_base_url, user_agent
from src.open_irceline.api import IrcelineRioClient from src.open_irceline.api import IrcelineRioClient
from src.open_irceline.data import RioFeature, FeatureValue from src.open_irceline.data import RioFeature, FeatureValue
from src.open_irceline.utils import epsg_transform 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")) @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(): async def test_api_rio():
pos = (50.4657, 4.8647) pos = (50.4657, 4.8647)
x, y = epsg_transform(pos) x, y = epsg_transform(pos)
session = get_mock_session_json('rio_wfs.json') session = get_mock_session('rio_wfs.json')
client = IrcelineRioClient(session) client = IrcelineRioClient(session)
@ -117,7 +117,7 @@ async def test_api_rio():
async def test_api_rio_get_capabilities(): 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) client = IrcelineRioClient(session)
_ = await client.get_rio_capabilities() _ = await client.get_rio_capabilities()

View file

@ -1,7 +1,12 @@
from datetime import date, timedelta, datetime
from random import randint, seed from random import randint, seed
from freezegun import freeze_time
import pytest 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 src.open_irceline.data import BelAqiIndex
from tests.conftest import get_mock_session_many_csv, get_mock_session
def test_belaqi_index(): def test_belaqi_index():
@ -140,4 +145,53 @@ def test_belaqi_value_error():
with pytest.raises(ValueError): with pytest.raises(ValueError):
belaqi_index(1, 0, 12, -8888) belaqi_index(1, 0, 12, -8888)
# TODO add more test for the other BelAQI functions
@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)