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
|
|
@ -3,3 +3,4 @@ pyproj==3.6.1
|
|||
pytest
|
||||
async-timeout==4.0.3
|
||||
pytest-asyncio==0.23.7
|
||||
freezegun
|
|
@ -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
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