First implementation of get RIO feature

This commit is contained in:
Jules 2024-06-15 19:33:52 +02:00
parent 2591481bc5
commit 8d9532170e
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
6 changed files with 148 additions and 0 deletions

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
aiohttp==.3.9.5
pyproj==3.6.1
pytest
async-timeout==4.0.3
pytest-asyncio==0.23.7

5
setup.cfg Normal file
View file

@ -0,0 +1,5 @@
[tool:pytest]
testpaths = tests
norecursedirs = .git
addopts = -s -v
asyncio_mode = auto

View file

@ -0,0 +1,4 @@
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'

107
src/open_irceline/api.py Normal file
View file

@ -0,0 +1,107 @@
import asyncio
import socket
from datetime import datetime, timedelta
from typing import Tuple, List
import aiohttp
from aiohttp import ClientResponse
import async_timeout
from . import project_transform, rio_wfs_base_url
from .data import RioFeature, FeatureValue
class IrcelineApiError(Exception):
"""Exception to indicate a general API error."""
class IrcelineApiCommunicationError(IrcelineApiError):
"""Exception to indicate a communication error."""
class IrcelineApiParametersError(IrcelineApiError):
"""Exception to indicate a parameter error."""
class IrcelineClient:
"""API client for IRCEL - CELINE open data"""
def __init__(self, session: aiohttp.ClientSession) -> None:
self._session = session
@staticmethod
def epsg_transform(position: Tuple[float, float]) -> tuple:
"""
Convert 'EPSG:4326' coordinates to 'EPSG:31370' coordinates
:param position: (x, y) coordinates
:return: tuple of int in the EPSG:31370 system
"""
result = project_transform.transform(position[0], position[1])
return round(result[0]), round(result[1])
async def get_rio_value(self,
timestamp: datetime,
features: List[RioFeature],
position: Tuple[float, float]
) -> dict:
coord = self.epsg_transform(position)
# Remove one hour 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)
timestamp = timestamp.replace(microsecond=0, second=0, minute=0) - timedelta(hours=1)
querystring = {"service": "WFS",
"version": "1.3.0",
"request": "GetFeature",
"outputFormat": "application/json",
"typeName": ",".join(features),
"cql_filter": f"timestamp>='{timestamp.isoformat()}' AND "
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())
@staticmethod
def format_result(prefix: str, data: dict) -> dict:
if data.get('type', None) != 'FeatureCollection' or not isinstance(data.get('features', None), list):
return dict()
features = data.get('features', [])
result = dict()
for f in features:
if (f.get('id', None) is None or
f.get('properties', {}).get('value', None) is None or
f.get('properties', {}).get('timestamp', None) is None):
continue
try:
timestamp = datetime.fromisoformat(f.get('properties', {}).get('timestamp'))
value = float(f.get('properties', {}).get('value'))
except (TypeError, ValueError):
continue
name = f"{prefix}:{f.get('id').split('.')[0]}"
if name not in result or result[name]['timestamp'] < timestamp:
result[name] = FeatureValue(timestamp=timestamp, value=value)
return result
async def _api_wrapper(self, url: str, querystring: dict):
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 IrcelineApiCommunicationError("Timeout error fetching information") from exception
except (aiohttp.ClientError, socket.gaierror) as exception:
raise IrcelineApiCommunicationError("Error fetching information") from exception
except Exception as exception: # pylint: disable=broad-except
raise IrcelineApiError(f"Something really wrong happened! {exception}") from exception

27
src/open_irceline/data.py Normal file
View file

@ -0,0 +1,27 @@
from enum import StrEnum
from typing import TypedDict
from datetime import datetime
class RioFeature(StrEnum):
BC_HMEAN = 'rio:bc_hmean'
BC_24HMEAN = 'rio:bc_24hmean'
BC_DMEAN = 'rio:bc_dmean'
NO2_HMEAN = 'rio:no2_hmean'
NO2_DMEAN = 'rio:no2_dmean'
O3_HMEAN = 'rio:o3_hmean'
O3_MAXHMEAN = 'rio:o3_maxhmean'
O3_8HMEAN = 'rio:o3_8hmean'
O3_MAX8HMEAN = 'rio:o3_max8hmean'
PM10_HMEAN = 'rio:pm10_hmean'
PM10_24HMEAN = 'rio:pm10_24hmean'
PM10_DMEAN = 'rio:pm10_dmean'
PM25_HMEAN = 'rio:pm25_hmean'
PM25_24HMEAN = 'rio:pm25_24hmean'
PM25_DMEAN = 'rio:pm25_dmean'
SO2_HMEAN = 'rio:so2_hmean'
class FeatureValue(TypedDict):
timestamp: datetime | None
value: int | float | None

0
tests/__init__.py Normal file
View file