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