Skip to content

Commit 1e3ba96

Browse files
committed
New Air Pollution retrieval API supported - fixes #362
1 parent 55c7f33 commit 1e3ba96

File tree

8 files changed

+346
-50
lines changed

8 files changed

+346
-50
lines changed

pyowm/airpollutionapi30/airpollution_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33

4-
from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL
4+
from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL, AIR_POLLUTION_URL
55
from pyowm.utils import formatting
66

77

@@ -152,6 +152,18 @@ def get_so2(self, params_dict):
152152
_, json_data = self._client.get_json(uri)
153153
return json_data
154154

155+
def get_air_pollution(self, params_dict):
156+
"""
157+
Invokes the new AirPollution API endpoint
158+
159+
:param params_dict: dict of parameters
160+
:returns: a string containing raw JSON data
161+
:raises: *ValueError*, *APIRequestError*
162+
163+
"""
164+
_, json_data = self._client.get_json(AIR_POLLUTION_URL, params=params_dict)
165+
return json_data
166+
155167
def __repr__(self):
156168
return "<%s.%s - httpclient=%s>" % \
157169
(__name__, self.__class__.__name__, str(self._client))

pyowm/airpollutionapi30/airpollution_manager.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33

4-
from pyowm.airpollutionapi30 import airpollution_client, coindex, no2index, ozone, so2index
5-
from pyowm.airpollutionapi30.uris import ROOT_POLLUTION_API_URL
4+
from pyowm.airpollutionapi30 import airpollution_client, coindex, no2index, ozone, so2index, airstatus
5+
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
8+
from pyowm.utils import geo, decorators
99

1010

1111
class AirPollutionManager:
@@ -29,10 +29,14 @@ def __init__(self, API_key, config):
2929
self.ap_client = airpollution_client.AirPollutionHttpClient(
3030
API_key,
3131
HttpClient(API_key, config, ROOT_POLLUTION_API_URL))
32+
self.new_ap_client = airpollution_client.AirPollutionHttpClient(
33+
API_key,
34+
HttpClient(API_key, config, NEW_ROOT_POLLUTION_API_URL))
3235

3336
def airpollution_api_version(self):
3437
return AIRPOLLUTION_API_VERSION
3538

39+
@decorators.deprecated('removed', '4', 'coindex_around_coords')
3640
def coindex_around_coords(self, lat, lon, start=None, interval=None):
3741
"""
3842
Queries the OWM AirPollution API for Carbon Monoxide values sampled in the
@@ -72,6 +76,7 @@ def coindex_around_coords(self, lat, lon, start=None, interval=None):
7276
coi.interval = interval
7377
return coi
7478

79+
@decorators.deprecated('removed', '4', 'ozone_around_coords')
7580
def ozone_around_coords(self, lat, lon, start=None, interval=None):
7681
"""
7782
Queries the OWM AirPollution API for Ozone (O3) value in Dobson Units sampled in
@@ -110,6 +115,7 @@ def ozone_around_coords(self, lat, lon, start=None, interval=None):
110115
oz.interval = interval
111116
return oz
112117

118+
@decorators.deprecated('removed', '4', 'no2index_around_coords')
113119
def no2index_around_coords(self, lat, lon, start=None, interval=None):
114120
"""
115121
Queries the OWM AirPollution API for Nitrogen Dioxide values sampled in the
@@ -149,6 +155,7 @@ def no2index_around_coords(self, lat, lon, start=None, interval=None):
149155
no2.interval = interval
150156
return no2
151157

158+
@decorators.deprecated('removed', '4', 'so2index_around_coords')
152159
def so2index_around_coords(self, lat, lon, start=None, interval=None):
153160
"""
154161
Queries the OWM AirPollution API for Sulphur Dioxide values sampled in the
@@ -188,5 +195,25 @@ def so2index_around_coords(self, lat, lon, start=None, interval=None):
188195
so2.interval = interval
189196
return so2
190197

198+
def air_quality_at_coords(self, lat, lon):
199+
"""
200+
Queries the OWM AirPollution API for all available air quality indicators around the specified coordinates.
201+
202+
:param lat: the location's latitude, must be between -90.0 and 90.0
203+
:type lat: int/float
204+
:param lon: the location's longitude, must be between -180.0 and 180.0
205+
:type lon: int/float
206+
:return: a *AirStatus* instance or ``None`` if data is not available
207+
:raises: *ParseResponseException* when OWM AirPollution API responses' data
208+
cannot be parsed, *APICallException* when OWM AirPollution API can not be
209+
reached, *ValueError* for wrong input values
210+
"""
211+
geo.assert_is_lon(lon)
212+
geo.assert_is_lat(lat)
213+
params = {'lon': lon, 'lat': lat}
214+
json_data = self.new_ap_client.get_air_pollution(params)
215+
air_status = airstatus.AirStatus.from_dict(json_data)
216+
return air_status
217+
191218
def __repr__(self):
192219
return '<%s.%s>' % (__name__, self.__class__.__name__)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from pyowm.commons import exceptions
5+
from pyowm.utils import formatting, timestamps
6+
from pyowm.weatherapi25 import location
7+
8+
9+
class AirStatus:
10+
"""
11+
A class representing a dataset about air quality
12+
13+
:param reference_time: GMT UNIXtime telling when the data has been measured
14+
:type reference_time: int
15+
:param location: the *Location* relative to this measurement
16+
:type location: *Location*
17+
:param interval: the time granularity of the CO observation
18+
:type interval: str
19+
:param air_quality_data: the dataset
20+
:type air_quality_data: dict
21+
:param reception_time: GMT UNIXtime telling when the CO observation has
22+
been received from the OWM Weather API
23+
:type reception_time: int
24+
:returns: an *COIndex* instance
25+
:raises: *ValueError* when negative values are provided as reception time,
26+
CO samples are not provided in a list
27+
28+
"""
29+
30+
def __init__(self, reference_time, location, air_quality_data, reception_time):
31+
if reference_time < 0:
32+
raise ValueError("'reference_time' must be greater than 0")
33+
self.ref_time = reference_time
34+
self.location = location
35+
if not isinstance(air_quality_data, dict):
36+
raise ValueError("'air_quality_data' must be a list")
37+
self.air_quality_data = air_quality_data
38+
for key, val in air_quality_data.items():
39+
setattr(self, key, val)
40+
if reception_time < 0:
41+
raise ValueError("'reception_time' must be greater than 0")
42+
self.rec_time = reception_time
43+
44+
def reference_time(self, timeformat='unix'):
45+
"""
46+
Returns the GMT time telling when the air quality data have been measured
47+
48+
:param timeformat: the format for the time value. May be:
49+
'*unix*' (default) for UNIX time
50+
'*iso*' for ISO8601-formatted string in the format ``YYYY-MM-DD HH:MM:SS+00:00``
51+
'*date* for ``datetime.datetime`` object instance
52+
:type timeformat: str
53+
:returns: an int or a str
54+
:raises: ValueError when negative values are provided
55+
56+
"""
57+
return formatting.timeformat(self.ref_time, timeformat)
58+
59+
def reception_time(self, timeformat='unix'):
60+
"""
61+
Returns the GMT time telling when the air quality data has been received
62+
from the OWM Weather API
63+
64+
:param timeformat: the format for the time value. May be:
65+
'*unix*' (default) for UNIX time
66+
'*iso*' for ISO8601-formatted string in the format ``YYYY-MM-DD HH:MM:SS+00:00``
67+
'*date* for ``datetime.datetime`` object instance
68+
:type timeformat: str
69+
:returns: an int or a str
70+
:raises: ValueError when negative values are provided
71+
72+
"""
73+
return formatting.timeformat(self.rec_time, timeformat)
74+
75+
76+
@classmethod
77+
def from_dict(cls, the_dict):
78+
"""
79+
Parses a *AirStatus* instance out of a data dictionary.
80+
81+
:param the_dict: the input dictionary
82+
:type the_dict: `dict`
83+
:returns: a *AirStatus* instance or ``None`` if no data is available
84+
:raises: *ParseAPIResponseError* if it is impossible to find or parse the data needed to build the result
85+
86+
"""
87+
if the_dict is None:
88+
raise exceptions.ParseAPIResponseError('Data is None')
89+
try:
90+
91+
item = the_dict['list'][0]
92+
93+
# -- reference time (strip away Z and T on ISO8601 format)
94+
reference_time = item['dt']
95+
96+
# -- reception time (now)
97+
reception_time = timestamps.now('unix')
98+
99+
# -- location
100+
lon = float(the_dict['coord']['lat'])
101+
lat = float(the_dict['coord']['lon'])
102+
place = location.Location(None, lon, lat, None)
103+
104+
# -- air quality data
105+
data = item['components']
106+
data['aqi'] = item['main']['aqi']
107+
108+
except KeyError:
109+
raise exceptions.ParseAPIResponseError(
110+
''.join([__name__, ': impossible to parse AirStatus']))
111+
112+
return AirStatus(reference_time, place, data, reception_time)
113+
114+
def to_dict(self):
115+
"""Dumps object to a dictionary
116+
117+
:returns: a `dict`
118+
119+
"""
120+
return {"reference_time": self.ref_time,
121+
"location": self.location.to_dict(),
122+
"air_quality_data": self.air_quality_data,
123+
"reception_time": self.rec_time}
124+
125+
def __repr__(self):
126+
return "<%s.%s - reference time=%s, reception time=%s, location=%s" % (
127+
__name__,
128+
self.__class__.__name__,
129+
self.reference_time('iso'),
130+
self.reception_time('iso'),
131+
str(self.location))

pyowm/airpollutionapi30/uris.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33

4+
# deprecated API endpoints
45
ROOT_POLLUTION_API_URL = 'openweathermap.org/pollution/v1'
56
CO_INDEX_URL = 'co'
67
OZONE_URL = 'o3'
78
NO2_INDEX_URL = 'no2'
89
SO2_INDEX_URL = 'so2'
10+
11+
12+
# current API endpoint
13+
NEW_ROOT_POLLUTION_API_URL = 'openweathermap.org/data/2.5'
14+
AIR_POLLUTION_URL = 'air_pollution'

sphinx/v3/code-recipes.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Table of contents:
88
* [Identifying cities and places via city IDs](#identifying_places)
99
* [OneCall data](#onecall)
1010
* [Weather data](#weather_data)
11+
* [Air pollution data](#airpollution_data)
1112
* [Weather forecasts](#weather_forecasts)
1213
* [Meteostation historic measurements](#station_measurements)
1314

@@ -754,6 +755,37 @@ TBD
754755
TBD
755756

756757

758+
<div id="airpollution_data"/>
759+
760+
## Air pollution data
761+
762+
Instead of getting a `weather_manager`, get from the main OWM object a `airpollution_manager` and use it
763+
764+
### Getting air polluting concentrations and Air Quality Index on geographic coords
765+
Air polluting agents concentration can be queried in one shot:
766+
767+
```python
768+
from pyowm.owm import OWM
769+
owm = OWM('your-api-key')
770+
mgr = owm.airpollution_manager()
771+
772+
air_status = mgr.air_quality_at_coords(51.507351, -0.127758) # London, GB
773+
774+
# you can then get values for all of these air pollutants
775+
air_status.co
776+
air_status.no
777+
air_status.no2
778+
air_status.o3
779+
air_status.so2
780+
air_status.pm2_5
781+
air_status.pm10
782+
air_status.nh3
783+
784+
# and for air quality index
785+
air_status.aqi
786+
```
787+
788+
757789

758790
<div id="station_measurements"/>
759791

tests/integration/pollutionapi30/test_integration_pollutionapi30.py

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,16 @@ class IntegrationTestsPollutionAPI30(unittest.TestCase):
1010

1111
__owm = owm.OWM(os.getenv('OWM_API_KEY', None)).airpollution_manager()
1212

13-
def test_coindex_around_coords(self):
13+
def test_air_quality_at_coords(self):
1414
"""
15-
Test feature: get CO index around geo-coordinates.
15+
Test feature: get all air quality data around geo-coordinates.
1616
"""
17-
u = self.__owm.coindex_around_coords(45, 9)
18-
self.assertIsNotNone(u)
19-
self.assertIsNotNone(u.co_samples)
20-
self.assertIsNotNone(u.reception_time())
21-
self.assertIsNotNone(u.reference_time())
22-
self.assertIsNone(u.interval)
23-
self.assertIsNotNone(u.location)
24-
25-
def test_ozone_around_coords(self):
26-
"""
27-
Test feature: get ozone around geo-coordinates.
28-
"""
29-
u = self.__owm.ozone_around_coords(0.0, 10.0, start='2016-12-31 12:55:55+00:00')
30-
self.assertIsNotNone(u)
31-
self.assertIsNotNone(u.du_value)
32-
self.assertIsNotNone(u.reception_time())
33-
self.assertIsNotNone(u.reference_time())
34-
self.assertIsNone(u.interval)
35-
self.assertIsNotNone(u.location)
36-
37-
def test_no2index_around_coords(self):
38-
"""
39-
Test feature: get NO2 index around geo-coordinates.
40-
"""
41-
u = self.__owm.no2index_around_coords(0.0, 10.0, start='2016-12-31 12:55:55+00:00')
42-
self.assertIsNotNone(u)
43-
self.assertIsNotNone(u.no2_samples)
44-
self.assertIsNotNone(u.reception_time())
45-
self.assertIsNotNone(u.reference_time())
46-
self.assertIsNone(u.interval)
47-
self.assertIsNotNone(u.location)
48-
49-
def test_so2index_around_coords(self):
50-
"""
51-
Test feature: get SO2 index around geo-coordinates.
52-
"""
53-
u = self.__owm.so2index_around_coords(0.0, 10.0, start='2016-12-31 12:55:55+00:00')
54-
self.assertIsNotNone(u)
55-
self.assertIsNotNone(u.so2_samples)
56-
self.assertIsNotNone(u.reception_time())
57-
self.assertIsNotNone(u.reference_time())
58-
self.assertIsNone(u.interval)
59-
self.assertIsNotNone(u.location)
17+
airstatus = self.__owm.air_quality_at_coords(45, 9)
18+
self.assertIsNotNone(airstatus)
19+
self.assertIsNotNone(airstatus.air_quality_data)
20+
self.assertIsNotNone(airstatus.reception_time())
21+
self.assertIsNotNone(airstatus.reference_time())
22+
self.assertIsNotNone(airstatus.location)
6023

6124

6225
if __name__ == "__main__":

0 commit comments

Comments
 (0)