Skip to content

Commit 52f283e

Browse files
committed
Air pollution history. Fixes #362
1 parent f68d6b7 commit 52f283e

File tree

6 files changed

+141
-2
lines changed

6 files changed

+141
-2
lines changed

pyowm/airpollutionapi30/airpollution_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44
from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL, AIR_POLLUTION_URL, \
5-
AIR_POLLUTION_FORECAST_URL
5+
AIR_POLLUTION_FORECAST_URL, AIR_POLLUTION_HISTORY_URL
66
from pyowm.utils import formatting
77

88

@@ -177,6 +177,18 @@ def get_forecast_air_pollution(self, params_dict):
177177
_, json_data = self._client.get_json(AIR_POLLUTION_FORECAST_URL, params=params_dict)
178178
return json_data
179179

180+
def get_historical_air_pollution(self, params_dict):
181+
"""
182+
Invokes the new AirPollution API history endpoint
183+
184+
:param params_dict: dict of parameters
185+
:returns: a string containing raw JSON data
186+
:raises: *ValueError*, *APIRequestError*
187+
188+
"""
189+
_, json_data = self._client.get_json(AIR_POLLUTION_HISTORY_URL, params=params_dict)
190+
return json_data
191+
180192
def __repr__(self):
181193
return "<%s.%s - httpclient=%s>" % \
182194
(__name__, self.__class__.__name__, str(self._client))

pyowm/airpollutionapi30/airpollution_manager.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pyowm.airpollutionapi30.uris import ROOT_POLLUTION_API_URL, NEW_ROOT_POLLUTION_API_URL
66
from pyowm.commons.http_client import HttpClient
77
from pyowm.constants import AIRPOLLUTION_API_VERSION
8-
from pyowm.utils import geo, decorators
8+
from pyowm.utils import geo, decorators, formatting, timestamps
99

1010

1111
class AirPollutionManager:
@@ -239,5 +239,42 @@ def air_quality_forecast_at_coords(self, lat, lon):
239239
except:
240240
return []
241241

242+
def air_quality_history_at_coords(self, lat, lon, start, end=None):
243+
"""
244+
Queries the OWM AirPollution API for available forecasted air quality indicators around the specified coordinates.
245+
246+
:param lat: the location's latitude, must be between -90.0 and 90.0
247+
:type lat: int/float
248+
:param lon: the location's longitude, must be between -180.0 and 180.0
249+
:type lon: int/float
250+
:param start: the object conveying the start value of the search time window
251+
:type start: int, ``datetime.datetime`` or ISO8601-formatted string
252+
:param end: the object conveying the end value of the search time window. Values in the future will be clipped
253+
to the current timestamp. Defaults to the current UNIX timestamp.
254+
:type end: int, ``datetime.datetime`` or ISO8601-formatted string
255+
:return: a `list` of *AirStatus* instances or an empty `list` if data is not available
256+
:raises: *ParseResponseException* when OWM AirPollution API responses' data
257+
cannot be parsed, *APICallException* when OWM AirPollution API can not be
258+
reached, *ValueError* for wrong input values
259+
"""
260+
geo.assert_is_lon(lon)
261+
geo.assert_is_lat(lat)
262+
now = timestamps.now(timeformat='unix')
263+
assert start is not None
264+
start = formatting.timeformat(start, 'unix')
265+
if end is None:
266+
end = now
267+
else:
268+
end = formatting.timeformat(end, 'unix')
269+
if end > now:
270+
end = now
271+
272+
params = {'lon': lon, 'lat': lat, 'start': start, 'end': end}
273+
json_data = self.new_ap_client.get_historical_air_pollution(params)
274+
try:
275+
return airstatus.AirStatus.from_dict(json_data)
276+
except:
277+
return []
278+
242279
def __repr__(self):
243280
return '<%s.%s>' % (__name__, self.__class__.__name__)

pyowm/airpollutionapi30/uris.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
NEW_ROOT_POLLUTION_API_URL = 'openweathermap.org/data/2.5'
1414
AIR_POLLUTION_URL = 'air_pollution'
1515
AIR_POLLUTION_FORECAST_URL = 'air_pollution/forecast'
16+
AIR_POLLUTION_HISTORY_URL = 'air_pollution/history'

sphinx/v3/code-recipes.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,36 @@ for air_status in list_of_forecasts:
878878
air_status.aqi # air quality index
879879
```
880880

881+
### Getting historical air pollution data on geographic coords
882+
We can get also get historical values for air pollution agents concentration and air quality index:
883+
884+
```python
885+
from pyowm.owm import OWM
886+
owm = OWM('your-api-key')
887+
mgr = owm.airpollution_manager()
888+
889+
# fetch history from a certain point in time up to now...
890+
start = 1606223802 # November 24, 2020
891+
list_of_historical_values = mgr.air_quality_history_at_coords(51.507351, -0.127758, start) # London, GB
892+
893+
# ...or fetch history on a closed timeframe in the past
894+
end = 1613864065 # February 20, 2021
895+
list_of_historical_values = mgr.air_quality_history_at_coords(51.507351, -0.127758, start, end=end) # London, GB
896+
897+
# Each item in the list_of_historical_values is an AirStatus object
898+
for air_status in list_of_historical_values:
899+
air_status.co
900+
air_status.no
901+
air_status.no2
902+
air_status.o3
903+
air_status.so2
904+
air_status.pm2_5
905+
air_status.pm10
906+
air_status.nh3
907+
air_status.aqi # air quality index
908+
```
909+
910+
881911

882912
<div id="station_measurements"/>
883913

tests/integration/airpollutionapi30/test_integration_pollutionapi30.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ def test_air_quality_forecast_at_coords(self):
3333
self.assertIsNotNone(airstatus.reference_time())
3434
self.assertIsNotNone(airstatus.location)
3535

36+
def test_air_quality_history_at_coords(self):
37+
"""
38+
Test feature: get historical air quality data around geo-coordinates.
39+
"""
40+
start = 1606223802 # Tuesday, November 24, 2020
41+
42+
list_of_airstatuses = self.__owm.air_quality_history_at_coords(45, 9, start)
43+
self.assertIsInstance(list_of_airstatuses, list)
44+
for airstatus in list_of_airstatuses:
45+
self.assertIsNotNone(airstatus.air_quality_data)
46+
self.assertIsNotNone(airstatus.reception_time())
47+
self.assertIsNotNone(airstatus.reference_time())
48+
self.assertIsNotNone(airstatus.location)
49+
3650

3751
if __name__ == "__main__":
3852
unittest.main()

tests/unit/airpollutionapi30/test_airpollution_manager.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pyowm.airpollutionapi30 import airpollution_client, airpollution_manager, coindex, so2index, ozone, no2index, airstatus
77
from pyowm.config import DEFAULT_CONFIG
88
from pyowm.constants import AIRPOLLUTION_API_VERSION
9+
from pyowm.utils import timestamps
910
from tests.unit.airpollutionapi30.test_ozone import OZONE_JSON
1011
from tests.unit.airpollutionapi30.test_coindex import COINDEX_JSON
1112
from tests.unit.airpollutionapi30.test_no2index import NO2INDEX_JSON
@@ -32,6 +33,9 @@ def mock_get_air_pollution(self, params_dict):
3233
def mock_get_forecast_air_pollution(self, params_dict):
3334
return json.loads(AIRSTATUS_MULTIPLE_JSON)
3435

36+
def mock_get_historical_air_pollution(self, params_dict):
37+
return json.loads(AIRSTATUS_MULTIPLE_JSON)
38+
3539
def mock_get_so2_returning_so2index_around_coords(self, params_dict):
3640
return json.loads(SO2INDEX_JSON)
3741

@@ -229,5 +233,46 @@ def test_air_quality_forecast_at_coords_fails_with_wrong_parameters(self):
229233
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_forecast_at_coords, \
230234
self.__test_instance, 200, 2.5)
231235

236+
def test_air_quality_history_at_coords(self):
237+
ref_to_original = airpollution_client.AirPollutionHttpClient.get_historical_air_pollution
238+
airpollution_client.AirPollutionHttpClient.get_historical_air_pollution = \
239+
self.mock_get_historical_air_pollution
240+
result = self.__test_instance.air_quality_history_at_coords(45, 9, 12345678)
241+
airpollution_client.AirPollutionHttpClient.get_historical_air_pollution = ref_to_original
242+
self.assertTrue(isinstance(result, list))
243+
for item in result:
244+
self.assertIsInstance(item, airstatus.AirStatus)
245+
self.assertIsNotNone(item.reference_time)
246+
self.assertIsNotNone(item.reception_time())
247+
loc = item.location
248+
self.assertIsNotNone(loc)
249+
self.assertIsNotNone(loc.lat)
250+
self.assertIsNotNone(loc.lon)
251+
self.assertIsNotNone(item.air_quality_data)
252+
253+
def test_air_quality_history_at_coords_fails_with_wrong_parameters(self):
254+
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \
255+
self.__test_instance, 43.7, -200.0, 12345678, 12349999)
256+
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \
257+
self.__test_instance, 43.7, 200.0, 12345678, 12349999)
258+
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \
259+
self.__test_instance, -200, 2.5, 12345678, 12349999)
260+
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \
261+
self.__test_instance, 200, 2.5, 12345678, 12349999)
262+
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \
263+
self.__test_instance, 200, 2.5, 'test')
264+
self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \
265+
self.__test_instance, 200, 2.5, 'test', 'test2')
266+
267+
def test_air_quality_history_at_coords_clips_end_param_to_current_timestamp(self):
268+
now = timestamps.now(timeformat='unix')
269+
end = now + 99999999999
270+
271+
def assert_clipped(obj, params_dict):
272+
self.assertEqual(params_dict['end'], now)
273+
274+
airpollution_client.AirPollutionHttpClient.get_historical_air_pollution = assert_clipped
275+
_ = self.__test_instance.air_quality_history_at_coords(45, 9, 12345678, end=end)
276+
232277
def test_repr(self):
233278
print(self.__test_instance)

0 commit comments

Comments
 (0)