mirror of
https://github.com/jdejaegh/python-irceline.git
synced 2025-06-27 03:35:56 +02:00
Add tests to improve code coverage
This commit is contained in:
parent
ddb95115a5
commit
6be82c942a
5 changed files with 175 additions and 8 deletions
|
@ -1,7 +1,7 @@
|
||||||
from pyproj import Transformer
|
from pyproj import Transformer
|
||||||
|
|
||||||
project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=False)
|
project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=False)
|
||||||
rio_wfs_base_url = 'https://geo.irceline.be/rio/wfs'
|
rio_wfs_base_url = 'https://geo.irceline.be/wfs'
|
||||||
# noinspection HttpUrlsUsage
|
# noinspection HttpUrlsUsage
|
||||||
# There is not HTTPS version of this endpoint
|
# There is not HTTPS version of this endpoint
|
||||||
forecast_base_url = 'http://ftp.irceline.be/forecast'
|
forecast_base_url = 'http://ftp.irceline.be/forecast'
|
||||||
|
|
|
@ -92,7 +92,6 @@ class IrcelineRioClient(IrcelineBaseClient):
|
||||||
:param position: decimal degrees pair of coordinates
|
:param position: decimal degrees pair of coordinates
|
||||||
:return: dict with the response (key is RioFeature, value is FeatureValue with actual value and timestamp)
|
:return: dict with the response (key is RioFeature, value is FeatureValue with actual value and timestamp)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Remove one hour/day from timestamp to handle case where the hour just passed but the data is not yet there
|
# Remove one hour/day from timestamp to handle case where the hour just passed but the data is not yet there
|
||||||
# (e.g. 5.01 PM, but the most recent data is for 4.00 PM)
|
# (e.g. 5.01 PM, but the most recent data is for 4.00 PM)
|
||||||
if isinstance(timestamp, datetime):
|
if isinstance(timestamp, datetime):
|
||||||
|
@ -106,7 +105,7 @@ class IrcelineRioClient(IrcelineBaseClient):
|
||||||
else:
|
else:
|
||||||
raise IrcelineApiError(f"Wrong parameter type for timestamp: {type(timestamp)}")
|
raise IrcelineApiError(f"Wrong parameter type for timestamp: {type(timestamp)}")
|
||||||
|
|
||||||
coord = epsg_transform(position)
|
lat, lon = epsg_transform(position)
|
||||||
querystring = {"service": "WFS",
|
querystring = {"service": "WFS",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"request": "GetFeature",
|
"request": "GetFeature",
|
||||||
|
@ -115,8 +114,7 @@ class IrcelineRioClient(IrcelineBaseClient):
|
||||||
"cql_filter":
|
"cql_filter":
|
||||||
f"{key}>='{timestamp}'"
|
f"{key}>='{timestamp}'"
|
||||||
f" AND "
|
f" AND "
|
||||||
f"INTERSECTS(the_geom, POINT ({coord[0]} {coord[1]}))"}
|
f"INTERSECTS(the_geom, POINT ({lat} {lon}))"}
|
||||||
|
|
||||||
r: ClientResponse = await self._api_wrapper(rio_wfs_base_url, querystring)
|
r: ClientResponse = await self._api_wrapper(rio_wfs_base_url, querystring)
|
||||||
return self._format_result('rio', await r.json(), features)
|
return self._format_result('rio', await r.json(), features)
|
||||||
|
|
||||||
|
@ -212,7 +210,6 @@ class IrcelineForecastClient(IrcelineBaseClient):
|
||||||
:return: dict where key is (ForecastFeature, date of the forecast) and value is a FeatureValue
|
:return: dict where key is (ForecastFeature, date of the forecast) and value is a FeatureValue
|
||||||
"""
|
"""
|
||||||
x, y = round_coordinates(position[0], position[1])
|
x, y = round_coordinates(position[0], position[1])
|
||||||
|
|
||||||
result = dict()
|
result = dict()
|
||||||
|
|
||||||
for feature, d in product(features, range(5)):
|
for feature, d in product(features, range(5)):
|
||||||
|
@ -223,6 +220,7 @@ class IrcelineForecastClient(IrcelineBaseClient):
|
||||||
except IrcelineApiError:
|
except IrcelineApiError:
|
||||||
# retry for the day before
|
# retry for the day before
|
||||||
yesterday = day - timedelta(days=1)
|
yesterday = day - 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)
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import Mock, AsyncMock
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_data(fixture: str, plain=False) -> str | dict:
|
def get_api_data(fixture: str, plain=False) -> str | dict:
|
||||||
|
@ -6,3 +10,46 @@ def get_api_data(fixture: str, plain=False) -> str | dict:
|
||||||
if plain:
|
if plain:
|
||||||
return file.read()
|
return file.read()
|
||||||
return json.load(file)
|
return json.load(file)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mock_session_json(json_file=None, text_file=None):
|
||||||
|
# Create the mock response
|
||||||
|
mock_response = Mock()
|
||||||
|
if json_file is not None:
|
||||||
|
mock_response.json = AsyncMock(return_value=get_api_data(json_file))
|
||||||
|
if text_file is not None:
|
||||||
|
mock_response.text = AsyncMock(return_value=get_api_data(text_file, plain=True))
|
||||||
|
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_response.headers = dict()
|
||||||
|
|
||||||
|
# Create the mock session
|
||||||
|
mock_session = Mock(aiohttp.ClientSession)
|
||||||
|
mock_session.request = AsyncMock(return_value=mock_response)
|
||||||
|
return mock_session
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_response(*args, **kwargs):
|
||||||
|
etag = 'my-etag-here'
|
||||||
|
mock_response = Mock()
|
||||||
|
if '20240619' not in kwargs.get('url', ''):
|
||||||
|
mock_response.status = 404
|
||||||
|
mock_response.raise_for_status = Mock(side_effect=aiohttp.ClientResponseError(Mock(), Mock()))
|
||||||
|
elif etag in kwargs.get('headers', {}).get('If-None-Match', ''):
|
||||||
|
mock_response.text = AsyncMock(return_value='')
|
||||||
|
mock_response.status = 304
|
||||||
|
else:
|
||||||
|
mock_response.text = AsyncMock(return_value=get_api_data('forecast.csv', plain=True))
|
||||||
|
mock_response.status = 200
|
||||||
|
|
||||||
|
if '20240619' in kwargs.get('url', ''):
|
||||||
|
mock_response.headers = {'ETag': etag}
|
||||||
|
else:
|
||||||
|
mock_response.headers = dict()
|
||||||
|
return mock_response
|
||||||
|
|
||||||
|
|
||||||
|
def get_mock_session_many_csv():
|
||||||
|
mock_session = Mock(aiohttp.ClientSession)
|
||||||
|
mock_session.request = AsyncMock(side_effect=create_mock_response)
|
||||||
|
return mock_session
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
from datetime import date
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
from src.open_irceline import forecast_base_url, user_agent
|
||||||
from src.open_irceline.api import IrcelineForecastClient
|
from src.open_irceline.api import IrcelineForecastClient
|
||||||
from tests.conftest import get_api_data
|
from src.open_irceline.data import ForecastFeature
|
||||||
|
from tests.conftest import get_api_data, get_mock_session_many_csv
|
||||||
|
|
||||||
|
|
||||||
def test_extract_from_csv():
|
def test_extract_from_csv():
|
||||||
|
@ -11,3 +16,76 @@ def test_extract_from_csv():
|
||||||
|
|
||||||
result = IrcelineForecastClient.extract_result_from_csv(23, 4, data)
|
result = IrcelineForecastClient.extract_result_from_csv(23, 4, data)
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cached_calls():
|
||||||
|
session = get_mock_session_many_csv()
|
||||||
|
client = IrcelineForecastClient(session)
|
||||||
|
|
||||||
|
_ = await client.get_forecasts(
|
||||||
|
day=date(2024, 6, 19),
|
||||||
|
features=[ForecastFeature.NO2_MAXHMEAN],
|
||||||
|
position=(50.45, 4.85)
|
||||||
|
)
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
call(method='GET',
|
||||||
|
url=f"{forecast_base_url}/BE_{ForecastFeature.NO2_MAXHMEAN}_20240619_d{i}.csv",
|
||||||
|
params=None,
|
||||||
|
headers={'User-Agent': user_agent}
|
||||||
|
) for i in range(5)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert session.request.call_count == 5
|
||||||
|
session.request.assert_has_calls(calls)
|
||||||
|
|
||||||
|
_ = await client.get_forecasts(
|
||||||
|
day=date(2024, 6, 19),
|
||||||
|
features=[ForecastFeature.NO2_MAXHMEAN],
|
||||||
|
position=(50.45, 4.85)
|
||||||
|
)
|
||||||
|
|
||||||
|
calls += [
|
||||||
|
call(method='GET',
|
||||||
|
url=f"{forecast_base_url}/BE_{ForecastFeature.NO2_MAXHMEAN}_20240619_d{i}.csv",
|
||||||
|
params=None,
|
||||||
|
headers={'User-Agent': user_agent, 'If-None-Match': 'my-etag-here'}
|
||||||
|
) for i in range(5)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert session.request.call_count == 10
|
||||||
|
session.request.assert_has_calls(calls)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_missed_cached_calls():
|
||||||
|
session = get_mock_session_many_csv()
|
||||||
|
client = IrcelineForecastClient(session)
|
||||||
|
|
||||||
|
r = await client.get_forecasts(
|
||||||
|
day=date(2024, 6, 21),
|
||||||
|
features=[ForecastFeature.NO2_MAXHMEAN],
|
||||||
|
position=(50.45, 4.85)
|
||||||
|
)
|
||||||
|
|
||||||
|
calls = list()
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
calls += [
|
||||||
|
call(method='GET',
|
||||||
|
url=f"{forecast_base_url}/BE_{ForecastFeature.NO2_MAXHMEAN}_20240621_d{i}.csv",
|
||||||
|
params=None,
|
||||||
|
headers={'User-Agent': user_agent}
|
||||||
|
),
|
||||||
|
call(method='GET',
|
||||||
|
url=f"{forecast_base_url}/BE_{ForecastFeature.NO2_MAXHMEAN}_20240620_d{i}.csv",
|
||||||
|
params=None,
|
||||||
|
headers={'User-Agent': user_agent}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert session.request.call_count == 10
|
||||||
|
session.request.assert_has_calls(calls)
|
||||||
|
|
||||||
|
for value in r.values():
|
||||||
|
assert value['value'] is None
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,11 @@ from datetime import datetime, date
|
||||||
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
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 tests.conftest import get_api_data
|
from src.open_irceline.utils import epsg_transform
|
||||||
|
from tests.conftest import get_api_data, get_mock_session_json
|
||||||
|
|
||||||
|
|
||||||
@freeze_time(datetime.fromisoformat("2024-06-15T16:55:03.419Z"))
|
@freeze_time(datetime.fromisoformat("2024-06-15T16:55:03.419Z"))
|
||||||
|
@ -86,3 +88,45 @@ def test_parse_capabilities():
|
||||||
def test_parse_capabilities_with_error():
|
def test_parse_capabilities_with_error():
|
||||||
result = IrcelineRioClient._parse_capabilities("wow there no valid XML")
|
result = IrcelineRioClient._parse_capabilities("wow there no valid XML")
|
||||||
assert result == set()
|
assert result == set()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_rio():
|
||||||
|
pos = (50.4657, 4.8647)
|
||||||
|
x, y = epsg_transform(pos)
|
||||||
|
session = get_mock_session_json('rio_wfs.json')
|
||||||
|
|
||||||
|
client = IrcelineRioClient(session)
|
||||||
|
|
||||||
|
d = date(2024, 6, 18)
|
||||||
|
features = [RioFeature.NO2_HMEAN, RioFeature.O3_HMEAN]
|
||||||
|
_ = await client.get_rio_value(d, features, pos)
|
||||||
|
session.request.assert_called_once_with(
|
||||||
|
method='GET',
|
||||||
|
url=rio_wfs_base_url,
|
||||||
|
params={"service": "WFS",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"request": "GetFeature",
|
||||||
|
"outputFormat": "application/json",
|
||||||
|
"typeName": ",".join(features),
|
||||||
|
"cql_filter":
|
||||||
|
f"date>='2024-06-17'"
|
||||||
|
f" AND "
|
||||||
|
f"INTERSECTS(the_geom, POINT ({x} {y}))"},
|
||||||
|
headers={'User-Agent': user_agent}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_rio_get_capabilities():
|
||||||
|
session = get_mock_session_json(text_file='capabilities.xml')
|
||||||
|
|
||||||
|
client = IrcelineRioClient(session)
|
||||||
|
_ = await client.get_rio_capabilities()
|
||||||
|
|
||||||
|
session.request.assert_called_once_with(
|
||||||
|
method='GET',
|
||||||
|
url=rio_wfs_base_url,
|
||||||
|
params={"service": "WFS",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"request": "GetCapabilities"},
|
||||||
|
headers={'User-Agent': user_agent}
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue