diff --git a/src/open_irceline/__init__.py b/src/open_irceline/__init__.py index a791cf4..2fc5c75 100644 --- a/src/open_irceline/__init__.py +++ b/src/open_irceline/__init__.py @@ -2,3 +2,7 @@ from pyproj import Transformer project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=True) rio_wfs_base_url = 'https://geo.irceline.be/rio/wfs' +# noinspection HttpUrlsUsage +# There is not HTTPS version of this endpoint +forecast_base_url = 'http://ftp.irceline.be/forecast' +user_agent = 'github.com/jdejaegh/python-irceline' diff --git a/src/open_irceline/api.py b/src/open_irceline/api.py index b07773d..815ff23 100644 --- a/src/open_irceline/api.py +++ b/src/open_irceline/api.py @@ -1,14 +1,14 @@ import asyncio import socket -from xml.etree import ElementTree from datetime import datetime, timedelta, date from typing import Tuple, List, Dict, Set +from xml.etree import ElementTree import aiohttp import async_timeout from aiohttp import ClientResponse -from . import project_transform, rio_wfs_base_url +from . import project_transform, rio_wfs_base_url, user_agent from .data import RioFeature, FeatureValue @@ -16,14 +16,44 @@ class IrcelineApiError(Exception): """Exception to indicate an API error.""" -class IrcelineClient: - """API client for IRCEL - CELINE open data""" - +class IrcelineBaseClient: def __init__(self, session: aiohttp.ClientSession) -> None: self._session = session + async def _api_wrapper(self, url: str, querystring: dict, method: str = 'GET'): + """ + Call the URL with the specified query string. Raises exception for >= 400 response code + :param url: base URL + :param querystring: dict to build the query string + :return: response from the client + """ + + headers = {'User-Agent': user_agent} + + try: + async with async_timeout.timeout(60): + response = await self._session.request( + method=method, + url=url, + params=querystring, + headers=headers + ) + response.raise_for_status() + return response + + except asyncio.TimeoutError as exception: + raise IrcelineApiError("Timeout error fetching information") from exception + except (aiohttp.ClientError, socket.gaierror) as exception: + raise IrcelineApiError("Error fetching information") from exception + except Exception as exception: # pylint: disable=broad-except + raise IrcelineApiError(f"Something really wrong happened! {exception}") from exception + + +class IrcelineRioClient(IrcelineBaseClient): + """API client for RIO interpolated IRCEL - CELINE open data""" + @staticmethod - def epsg_transform(position: Tuple[float, float]) -> tuple: + def _epsg_transform(position: Tuple[float, float]) -> tuple: """ Convert 'EPSG:4326' coordinates to 'EPSG:31370' coordinates :param position: (x, y) coordinates @@ -58,7 +88,7 @@ class IrcelineClient: else: raise IrcelineApiError(f"Wrong parameter type for timestamp: {type(timestamp)}") - coord = self.epsg_transform(position) + coord = self._epsg_transform(position) querystring = {"service": "WFS", "version": "1.3.0", "request": "GetFeature", @@ -70,7 +100,7 @@ class IrcelineClient: f"INTERSECTS(the_geom, POINT ({coord[0]} {coord[1]}))"} 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) async def get_rio_capabilities(self) -> Set[str]: """ @@ -82,10 +112,10 @@ class IrcelineClient: "request": "GetCapabilities"} r: ClientResponse = await self._api_wrapper(rio_wfs_base_url, querystring) - return self.parse_capabilities(await r.text()) + return self._parse_capabilities(await r.text()) @staticmethod - def parse_capabilities(xml_string: str) -> Set[str]: + def _parse_capabilities(xml_string: str) -> Set[str]: """ From an XML string obtained with GetCapabilities, generate a set of feature names :param xml_string: XML string to parse @@ -105,7 +135,7 @@ class IrcelineClient: return feature_type_names @staticmethod - def format_result(prefix: str, data: dict, features: List[RioFeature]) -> dict: + def _format_result(prefix: str, data: dict, features: List[RioFeature]) -> dict: """ Format the JSON dict returned by the WFS service into a more practical dict to use with only the latest measure for each feature requested @@ -145,30 +175,3 @@ class IrcelineClient: return result - async def _api_wrapper(self, url: str, querystring: dict): - """ - Call the URL with the specified query string (GET). Raises exception for >= 400 response code - :param url: base URL - :param querystring: dict to build the query string - :return: response from the client - """ - - headers = {'User-Agent': 'github.com/jdejaegh/python-irceline'} - - try: - async with async_timeout.timeout(60): - response = await self._session.request( - method='GET', - url=url, - params=querystring, - headers=headers - ) - response.raise_for_status() - return response - - except asyncio.TimeoutError as exception: - raise IrcelineApiError("Timeout error fetching information") from exception - except (aiohttp.ClientError, socket.gaierror) as exception: - raise IrcelineApiError("Error fetching information") from exception - except Exception as exception: # pylint: disable=broad-except - raise IrcelineApiError(f"Something really wrong happened! {exception}") from exception diff --git a/tests/test_api.py b/tests/test_api.py index adc5c76..7b84e94 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,7 +2,7 @@ from datetime import datetime, date from freezegun import freeze_time -from src.open_irceline.api import IrcelineClient +from src.open_irceline.api import IrcelineRioClient from src.open_irceline.data import RioFeature, FeatureValue from tests.conftest import get_api_data @@ -10,7 +10,7 @@ from tests.conftest import get_api_data @freeze_time(datetime.fromisoformat("2024-06-15T16:55:03.419Z")) async def test_format_result_hmean(): data = get_api_data('rio_wfs.json') - result = IrcelineClient.format_result('rio', data, [RioFeature.NO2_HMEAN, RioFeature.O3_HMEAN]) + result = IrcelineRioClient._format_result('rio', data, [RioFeature.NO2_HMEAN, RioFeature.O3_HMEAN]) expected = { str(RioFeature.O3_HMEAN): FeatureValue( @@ -29,8 +29,8 @@ async def test_format_result_hmean(): @freeze_time(datetime.fromisoformat("2024-06-15T19:30:09.581Z")) async def test_format_result_dmean(): data = get_api_data('rio_wfs_dmean.json') - result = IrcelineClient.format_result('rio', data, - [RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN]) + result = IrcelineRioClient._format_result('rio', data, + [RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN]) expected = { str(RioFeature.BC_DMEAN): FeatureValue(timestamp=date(2024, 6, 15), value=0.1), @@ -43,7 +43,7 @@ async def test_format_result_dmean(): def test_parse_capabilities(): with open('tests/fixtures/capabilities.xml', 'r') as xml_file: - result = IrcelineClient.parse_capabilities(xml_file.read()) + result = IrcelineRioClient._parse_capabilities(xml_file.read()) expected = {'rio:so2_anmean_be', 'rio:o3_hmean', 'rio:bc_anmean_vl', 'rio:o3_anmean_be', 'rio:pm10_hmean_vl', 'rio:o3_aot40for_be', 'rio:no2_maxhmean', 'rio:pm10_24hmean_1x1', 'rio:o3_aot40veg_5y_be',