mirror of
https://github.com/jdejaegh/python-irceline.git
synced 2025-06-27 03:35:56 +02:00
Add docstring and tests
This commit is contained in:
parent
8d9532170e
commit
7248caed49
6 changed files with 339 additions and 10 deletions
32
.github/workflows/pytest.yml
vendored
Normal file
32
.github/workflows/pytest.yml
vendored
Normal 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
|
|
@ -2,4 +2,5 @@ aiohttp==.3.9.5
|
||||||
pyproj==3.6.1
|
pyproj==3.6.1
|
||||||
pytest
|
pytest
|
||||||
async-timeout==4.0.3
|
async-timeout==4.0.3
|
||||||
pytest-asyncio==0.23.7
|
pytest-asyncio==0.23.7
|
||||||
|
freezegun
|
|
@ -1,11 +1,11 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Tuple, List
|
from typing import Tuple, List, Dict
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import ClientResponse
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
from aiohttp import ClientResponse
|
||||||
|
|
||||||
from . import project_transform, rio_wfs_base_url
|
from . import project_transform, rio_wfs_base_url
|
||||||
from .data import RioFeature, FeatureValue
|
from .data import RioFeature, FeatureValue
|
||||||
|
@ -43,12 +43,19 @@ class IrcelineClient:
|
||||||
timestamp: datetime,
|
timestamp: datetime,
|
||||||
features: List[RioFeature],
|
features: List[RioFeature],
|
||||||
position: Tuple[float, float]
|
position: Tuple[float, float]
|
||||||
) -> dict:
|
) -> Dict[RioFeature, FeatureValue]:
|
||||||
coord = self.epsg_transform(position)
|
"""
|
||||||
|
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
|
# 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)
|
# (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)
|
timestamp = timestamp.replace(microsecond=0, second=0, minute=0) - timedelta(hours=1)
|
||||||
|
coord = self.epsg_transform(position)
|
||||||
querystring = {"service": "WFS",
|
querystring = {"service": "WFS",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"request": "GetFeature",
|
"request": "GetFeature",
|
||||||
|
@ -58,15 +65,23 @@ class IrcelineClient:
|
||||||
f"INTERSECTS(the_geom, POINT ({coord[0]} {coord[1]}))"}
|
f"INTERSECTS(the_geom, POINT ({coord[0]} {coord[1]}))"}
|
||||||
|
|
||||||
r: ClientResponse = await self._api_wrapper(rio_wfs_base_url, querystring)
|
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
|
@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):
|
if data.get('type', None) != 'FeatureCollection' or not isinstance(data.get('features', None), list):
|
||||||
return dict()
|
return dict()
|
||||||
features = data.get('features', [])
|
features_api = data.get('features', [])
|
||||||
result = dict()
|
result = dict()
|
||||||
for f in features:
|
for f in features_api:
|
||||||
if (f.get('id', None) is None or
|
if (f.get('id', None) is None or
|
||||||
f.get('properties', {}).get('value', None) is None or
|
f.get('properties', {}).get('value', None) is None or
|
||||||
f.get('properties', {}).get('timestamp', None) is None):
|
f.get('properties', {}).get('timestamp', None) is None):
|
||||||
|
@ -79,12 +94,20 @@ class IrcelineClient:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = f"{prefix}:{f.get('id').split('.')[0]}"
|
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:
|
if name not in result or result[name]['timestamp'] < timestamp:
|
||||||
result[name] = FeatureValue(timestamp=timestamp, value=value)
|
result[name] = FeatureValue(timestamp=timestamp, value=value)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _api_wrapper(self, url: str, querystring: dict):
|
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'}
|
headers = {'User-Agent': 'github.com/jdejaegh/python-irceline'}
|
||||||
|
|
||||||
|
|
6
tests/conftest.py
Normal file
6
tests/conftest.py
Normal 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
243
tests/fixtures/rio_wfs.json
vendored
Normal 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
24
tests/test_api.py
Normal 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
|
Loading…
Add table
Reference in a new issue