Handle daily mean values from WFS as well

This commit is contained in:
Jules 2024-06-15 21:37:22 +02:00
parent 7248caed49
commit c723d409e5
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
4 changed files with 300 additions and 16 deletions

View file

@ -1,6 +1,6 @@
import asyncio
import socket
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date
from typing import Tuple, List, Dict
import aiohttp
@ -40,29 +40,35 @@ class IrcelineClient:
return round(result[0]), round(result[1])
async def get_rio_value(self,
timestamp: datetime,
timestamp: datetime | date,
features: List[RioFeature],
position: Tuple[float, float]
) -> Dict[RioFeature, FeatureValue]:
"""
Call the WFS API to get the interpolated level of RioFeature
Call the WFS API to get the interpolated level of RioFeature. Raises exception upon API error
:param timestamp: datetime for which to get the data for
:param features: list of RioFeature to fetch from the API
:param position: decimal degrees pair of coordinates
:return: dict with the response (key is RioFeature, value is FeatureValue with actual value and timestamp)
"""
# Remove one hour from timestamp to handle case where the hour just passed but the data is not yet there
# Remove one hour/day 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)
if isinstance(timestamp, datetime):
timestamp = timestamp.replace(microsecond=0, second=0, minute=0) - timedelta(hours=1)
elif isinstance(timestamp, date):
timestamp = timestamp - timedelta(days=1)
coord = self.epsg_transform(position)
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]}))"}
"cql_filter":
f"{'timestamp' if isinstance(timestamp, datetime) else 'date'}>='{timestamp.isoformat()}'"
f" 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(), features)
@ -82,14 +88,21 @@ class IrcelineClient:
features_api = data.get('features', [])
result = dict()
for f in features_api:
props = f.get('properties', {})
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):
props.get('value', None) is None):
continue
if (props.get('timestamp', None) is None and
props.get('date', None) is None):
continue
try:
timestamp = datetime.fromisoformat(f.get('properties', {}).get('timestamp'))
value = float(f.get('properties', {}).get('value'))
if 'timestamp' in props.keys():
timestamp = datetime.fromisoformat(props.get('timestamp'))
else:
# Cut last character as the date is written '2024-06-15Z' which is not ISO compliant
timestamp = date.fromisoformat(props.get('date')[:-1])
value = float(props.get('value'))
except (TypeError, ValueError):
continue

View file

@ -1,6 +1,6 @@
from enum import StrEnum
from typing import TypedDict
from datetime import datetime
from datetime import datetime, date
class RioFeature(StrEnum):
@ -23,5 +23,5 @@ class RioFeature(StrEnum):
class FeatureValue(TypedDict):
timestamp: datetime | None
timestamp: datetime | date
value: int | float | None

243
tests/fixtures/rio_wfs_dmean.json vendored Normal file
View file

@ -0,0 +1,243 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "bc_dmean.fid-280be381_1901d555556_494e",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"date": "2024-06-14Z",
"value": 0.2,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "bc_dmean.fid-280be381_1901d555556_494f",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"date": "2024-06-15Z",
"value": 0.1,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "pm10_dmean.fid-280be381_1901d555556_4950",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"date": "2024-06-14Z",
"value": 4.8,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "pm10_dmean.fid-280be381_1901d555556_4951",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"date": "2024-06-15Z",
"value": 5.9,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "pm25_dmean.fid-280be381_1901d555556_4952",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"date": "2024-06-14Z",
"value": 1.3,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "pm25_dmean.fid-280be381_1901d555556_4953",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"date": "2024-06-15Z",
"value": 1.1,
"network": "Wallonia"
}
}
],
"totalFeatures": 6,
"numberMatched": 6,
"numberReturned": 6,
"timeStamp": "2024-06-15T19:30:09.581Z",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:EPSG::31370"
}
}
}

View file

@ -1,12 +1,15 @@
from datetime import datetime, date
from aiohttp import ClientSession
from freezegun import freeze_time
from src.open_irceline.api import IrcelineClient
from src.open_irceline.data import RioFeature, FeatureValue
from tests.conftest import get_api_data
from datetime import datetime
from freezegun import freeze_time
@freeze_time(datetime.fromisoformat("2024-06-15T16:55:03.419Z"))
async def test_format_result():
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])
@ -22,3 +25,28 @@ async def test_format_result():
}
assert result == expected
@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,
[RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN])
expected = {
str(RioFeature.BC_DMEAN): FeatureValue(timestamp=date(2024, 6, 15), value=0.1),
str(RioFeature.PM10_DMEAN): FeatureValue(timestamp=date(2024, 6, 15), value=5.9),
str(RioFeature.PM25_DMEAN): FeatureValue(timestamp=date(2024, 6, 15), value=1.1),
}
assert result == expected
async def test_run():
async with ClientSession() as session:
api = IrcelineClient(session)
r = await api.get_rio_value(
date.today(),
[RioFeature.BC_DMEAN, RioFeature.PM10_DMEAN, RioFeature.PM25_DMEAN],
(4.8637, 50.4656))
print(r)