Add docstring and tests

This commit is contained in:
Jules 2024-06-15 20:52:38 +02:00
parent 8d9532170e
commit 7248caed49
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
6 changed files with 339 additions and 10 deletions

32
.github/workflows/pytest.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Run Python tests
on:
push:
pull_request:
jobs:
build:
name: Run tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: MathRobin/timezone-action@v1.1
with:
timezoneLinux: "Europe/Brussels"
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install pytest pytest-md pytest-emoji
- name: Install requirements
run: pip install -r requirements.txt
- uses: pavelzw/pytest-action@v2
with:
emoji: false
verbose: false
job-summary: true

View file

@ -2,4 +2,5 @@ aiohttp==.3.9.5
pyproj==3.6.1
pytest
async-timeout==4.0.3
pytest-asyncio==0.23.7
pytest-asyncio==0.23.7
freezegun

View file

@ -1,11 +1,11 @@
import asyncio
import socket
from datetime import datetime, timedelta
from typing import Tuple, List
from typing import Tuple, List, Dict
import aiohttp
from aiohttp import ClientResponse
import async_timeout
from aiohttp import ClientResponse
from . import project_transform, rio_wfs_base_url
from .data import RioFeature, FeatureValue
@ -43,12 +43,19 @@ class IrcelineClient:
timestamp: datetime,
features: List[RioFeature],
position: Tuple[float, float]
) -> dict:
coord = self.epsg_transform(position)
) -> Dict[RioFeature, FeatureValue]:
"""
Call the WFS API to get the interpolated level of RioFeature
: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
# (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)
coord = self.epsg_transform(position)
querystring = {"service": "WFS",
"version": "1.3.0",
"request": "GetFeature",
@ -58,15 +65,23 @@ class IrcelineClient:
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())
return self.format_result('rio', await r.json(), features)
@staticmethod
def format_result(prefix: str, data: dict) -> 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
for each feature requested
:param prefix: namespace of the feature (e.g. rio), without the colon
:param data: JSON dict value as returned by the API
:param features: RioFeatures wanted in the final dict
:return: reduced dict, key is RioFeature, value is FeatureValue
"""
if data.get('type', None) != 'FeatureCollection' or not isinstance(data.get('features', None), list):
return dict()
features = data.get('features', [])
features_api = data.get('features', [])
result = dict()
for f in features:
for f in features_api:
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):
@ -79,12 +94,20 @@ class IrcelineClient:
continue
name = f"{prefix}:{f.get('id').split('.')[0]}"
if name not in [f'{f}' for f in features]:
continue
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):
"""
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'}

6
tests/conftest.py Normal file
View file

@ -0,0 +1,6 @@
import json
def get_api_data(fixture: str) -> dict:
with open(f'tests/fixtures/{fixture}', 'r') as file:
return json.load(file)

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

@ -0,0 +1,243 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "no2_hmean.fid-280be381_1901cca3e5c_4c81",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"timestamp": "2024-06-15T15:00:00Z",
"value": 4,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "no2_hmean.fid-280be381_1901cca3e5c_4c82",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"timestamp": "2024-06-15T16:00:00Z",
"value": 4,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "pm25_hmean.fid-280be381_1901cca3e5c_4c83",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"timestamp": "2024-06-15T15:00:00Z",
"value": 1,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "pm25_hmean.fid-280be381_1901cca3e5c_4c84",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"timestamp": "2024-06-15T16:00:00Z",
"value": 1,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "o3_hmean.fid-280be381_1901cca3e5c_4c85",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"timestamp": "2024-06-15T15:00:00Z",
"value": 74,
"network": "Wallonia"
}
},
{
"type": "Feature",
"id": "o3_hmean.fid-280be381_1901cca3e5c_4c86",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
182000,
128000
],
[
182000,
132000
],
[
186000,
132000
],
[
186000,
128000
],
[
182000,
128000
]
]
]
},
"geometry_name": "the_geom",
"properties": {
"id": 1102,
"timestamp": "2024-06-15T16:00:00Z",
"value": 71,
"network": "Wallonia"
}
}
],
"totalFeatures": 6,
"numberMatched": 6,
"numberReturned": 6,
"timeStamp": "2024-06-15T16:55:03.419Z",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:EPSG::31370"
}
}
}

24
tests/test_api.py Normal file
View file

@ -0,0 +1,24 @@
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():
data = get_api_data('rio_wfs.json')
result = IrcelineClient.format_result('rio', data, [RioFeature.NO2_HMEAN, RioFeature.O3_HMEAN])
expected = {
str(RioFeature.O3_HMEAN): FeatureValue(
timestamp=datetime.fromisoformat("2024-06-15T16:00:00Z"),
value=71
),
str(RioFeature.NO2_HMEAN): FeatureValue(
timestamp=datetime.fromisoformat("2024-06-15T16:00:00Z"),
value=4
)
}
assert result == expected