mirror of
https://github.com/jdejaegh/python-irceline.git
synced 2025-06-26 19:35:40 +02:00
First implementation of get RIO feature
This commit is contained in:
parent
2591481bc5
commit
8d9532170e
6 changed files with 148 additions and 0 deletions
5
requirements.txt
Normal file
5
requirements.txt
Normal 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
5
setup.cfg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
norecursedirs = .git
|
||||||
|
addopts = -s -v
|
||||||
|
asyncio_mode = auto
|
4
src/open_irceline/__init__.py
Normal file
4
src/open_irceline/__init__.py
Normal 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
107
src/open_irceline/api.py
Normal 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
27
src/open_irceline/data.py
Normal 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
0
tests/__init__.py
Normal file
Loading…
Add table
Reference in a new issue