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)
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 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

View file

@ -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,7 +29,7 @@ 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,
result = IrcelineRioClient._format_result('rio', data,
[RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN])
expected = {
@ -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',