Refactor API class

This commit is contained in:
Jules 2024-06-16 13:47:09 +02:00
parent e146c8359e
commit 800f87cf38
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
3 changed files with 50 additions and 43 deletions

View file

@ -2,3 +2,7 @@ from pyproj import Transformer
project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=True) project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=True)
rio_wfs_base_url = 'https://geo.irceline.be/rio/wfs' 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'

View file

@ -1,14 +1,14 @@
import asyncio import asyncio
import socket import socket
from xml.etree import ElementTree
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from typing import Tuple, List, Dict, Set from typing import Tuple, List, Dict, Set
from xml.etree import ElementTree
import aiohttp import aiohttp
import async_timeout import async_timeout
from aiohttp import ClientResponse 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 from .data import RioFeature, FeatureValue
@ -16,14 +16,44 @@ class IrcelineApiError(Exception):
"""Exception to indicate an API error.""" """Exception to indicate an API error."""
class IrcelineClient: class IrcelineBaseClient:
"""API client for IRCEL - CELINE open data"""
def __init__(self, session: aiohttp.ClientSession) -> None: def __init__(self, session: aiohttp.ClientSession) -> None:
self._session = session 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 @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 Convert 'EPSG:4326' coordinates to 'EPSG:31370' coordinates
:param position: (x, y) coordinates :param position: (x, y) coordinates
@ -58,7 +88,7 @@ class IrcelineClient:
else: else:
raise IrcelineApiError(f"Wrong parameter type for timestamp: {type(timestamp)}") raise IrcelineApiError(f"Wrong parameter type for timestamp: {type(timestamp)}")
coord = self.epsg_transform(position) coord = self._epsg_transform(position)
querystring = {"service": "WFS", querystring = {"service": "WFS",
"version": "1.3.0", "version": "1.3.0",
"request": "GetFeature", "request": "GetFeature",
@ -70,7 +100,7 @@ class IrcelineClient:
f"INTERSECTS(the_geom, POINT ({coord[0]} {coord[1]}))"} f"INTERSECTS(the_geom, POINT ({coord[0]} {coord[1]}))"}
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)
async def get_rio_capabilities(self) -> Set[str]: async def get_rio_capabilities(self) -> Set[str]:
""" """
@ -82,10 +112,10 @@ class IrcelineClient:
"request": "GetCapabilities"} "request": "GetCapabilities"}
r: ClientResponse = await self._api_wrapper(rio_wfs_base_url, querystring) 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 @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 From an XML string obtained with GetCapabilities, generate a set of feature names
:param xml_string: XML string to parse :param xml_string: XML string to parse
@ -105,7 +135,7 @@ class IrcelineClient:
return feature_type_names return feature_type_names
@staticmethod @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 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 for each feature requested
@ -145,30 +175,3 @@ class IrcelineClient:
return result 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

View file

@ -2,7 +2,7 @@ from datetime import datetime, date
from freezegun import freeze_time 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 src.open_irceline.data import RioFeature, FeatureValue
from tests.conftest import get_api_data 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")) @freeze_time(datetime.fromisoformat("2024-06-15T16:55:03.419Z"))
async def test_format_result_hmean(): async def test_format_result_hmean():
data = get_api_data('rio_wfs.json') 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 = { expected = {
str(RioFeature.O3_HMEAN): FeatureValue( 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")) @freeze_time(datetime.fromisoformat("2024-06-15T19:30:09.581Z"))
async def test_format_result_dmean(): async def test_format_result_dmean():
data = get_api_data('rio_wfs_dmean.json') data = get_api_data('rio_wfs_dmean.json')
result = IrcelineClient.format_result('rio', data, result = IrcelineRioClient._format_result('rio', data,
[RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN]) [RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN])
expected = { expected = {
str(RioFeature.BC_DMEAN): FeatureValue(timestamp=date(2024, 6, 15), value=0.1), 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(): def test_parse_capabilities():
with open('tests/fixtures/capabilities.xml', 'r') as xml_file: 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', 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', 'rio:o3_aot40for_be', 'rio:no2_maxhmean', 'rio:pm10_24hmean_1x1', 'rio:o3_aot40veg_5y_be',