mirror of
https://github.com/jdejaegh/python-irceline.git
synced 2025-06-27 03:35:56 +02:00
Refactor API class
This commit is contained in:
parent
e146c8359e
commit
800f87cf38
3 changed files with 50 additions and 43 deletions
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Add table
Reference in a new issue