From c10cec7b8160fd7768938eff83c77155dd13725f Mon Sep 17 00:00:00 2001 From: Darumin Date: Wed, 16 Sep 2020 02:29:19 -0700 Subject: [PATCH 01/36] started to work on more measurables for pressure/visibility --- .gitignore | 1 + CONTRIBUTORS.md | 1 + pyowm/utils/measurables.py | 23 +++++++++++++++++++++++ pyowm/weatherapi25/weather.py | 3 ++- tests/unit/utils/test_measurables.py | 8 ++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ca7f5a37..2f8c8441 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ tests/proxy/.tox/* .cache .eggs/ htmlcov/* +venv diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5fb7cae1..605ff2bd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,6 +6,7 @@ Contributors will be shown in alphabetical order Code ---- * [alechewitt](https://github.com/alechewitt) + * [Darumin](https://github.com/Darumin) * [dphildebrandt](https://github.com/dphildebrandt) * [dstmar](https://github.com/dstmar) * [edenhaus](https://github.com/edenhaus) diff --git a/pyowm/utils/measurables.py b/pyowm/utils/measurables.py index ec4421ac..0d58d0d4 100644 --- a/pyowm/utils/measurables.py +++ b/pyowm/utils/measurables.py @@ -11,6 +11,9 @@ KM_PER_HOUR_FOR_ONE_METER_PER_SEC = 3.6 KNOTS_FOR_ONE_METER_PER_SEC = 1.94384 +# Barometric conversion constants +HPA_FOR_ONE_INHG = 33.8639 + def kelvin_dict_to(d, target_temperature_unit): """ @@ -179,4 +182,24 @@ def metric_wind_dict_to_beaufort(d): result[key] = bf else: result[key] = value + return result + + +def metric_pressure_dict_to_inhg(d): + """ + Converts all pressure values in a dict to inches of mercury. + + :param d: the dictionary containing metric values + :type d: dict + :returns: a dict with the same keys as the input dict and values converted + to "Hg or inHg (inches of mercury) + + Note what OWM says about pressure: "Atmospheric pressure [is given in hPa] + (on the sea level, if there is no sea_level or grnd_level data)" + """ + result = dict() + for key, value in d.items(): + if value is None: + continue + result[key] = round((value / HPA_FOR_ONE_INHG), 2) return result \ No newline at end of file diff --git a/pyowm/weatherapi25/weather.py b/pyowm/weatherapi25/weather.py index a294a6af..759e4d57 100644 --- a/pyowm/weatherapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -505,4 +505,5 @@ def to_dict(self): 'humidex': self.humidex, 'heat_index': self.heat_index, 'utc_offset': self.utc_offset, - 'uvi': self.uvi} \ No newline at end of file + 'uvi': self.uvi} + diff --git a/tests/unit/utils/test_measurables.py b/tests/unit/utils/test_measurables.py index 25399896..11408369 100644 --- a/tests/unit/utils/test_measurables.py +++ b/tests/unit/utils/test_measurables.py @@ -132,3 +132,11 @@ def test_metric_wind_dict_to_beaufort(self): expected = {'speed': 8, 'gust': 2, 'deg': 7.89} result = measurables.metric_wind_dict_to_beaufort(input) self.assertEqual(expected, result) + + def test_metric_pressure_dict_to_inhg(self): + input = {'press': 1000, 'sea_level': 1, 'grnd_level': None} + expected = {'press': 29.53, 'sea_level': .03} + result = measurables.metric_pressure_dict_to_inhg(input) + print(result) + self.assertEqual(expected, result) + From 3918bc632c145d5e63c8bb446ad629116d049ff3 Mon Sep 17 00:00:00 2001 From: Darumin Date: Wed, 16 Sep 2020 16:52:24 -0700 Subject: [PATCH 02/36] attempt at implementing #325, support for visibility/pressure conversion --- pyowm/utils/measurables.py | 41 +++++++++++++++++++++++++--- tests/unit/utils/test_measurables.py | 10 +++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/pyowm/utils/measurables.py b/pyowm/utils/measurables.py index 0d58d0d4..77ba921f 100644 --- a/pyowm/utils/measurables.py +++ b/pyowm/utils/measurables.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*-""" -# Temperature coneversion constants +# Temperature conversion constants KELVIN_OFFSET = 273.15 FAHRENHEIT_OFFSET = 32.0 FAHRENHEIT_DEGREE_SCALE = 1.8 @@ -14,6 +14,13 @@ # Barometric conversion constants HPA_FOR_ONE_INHG = 33.8639 +# Visibility distance conversion constants +MILE_FOR_ONE_METER = 0.000621371 +KMS_FOR_ONE_METER = .001 + +# Decimal precision +ROUNDED_TO = 2 + def kelvin_dict_to(d, target_temperature_unit): """ @@ -187,7 +194,7 @@ def metric_wind_dict_to_beaufort(d): def metric_pressure_dict_to_inhg(d): """ - Converts all pressure values in a dict to inches of mercury. + Converts all barometric pressure values in a dict to "inches of mercury." :param d: the dictionary containing metric values :type d: dict @@ -201,5 +208,31 @@ def metric_pressure_dict_to_inhg(d): for key, value in d.items(): if value is None: continue - result[key] = round((value / HPA_FOR_ONE_INHG), 2) - return result \ No newline at end of file + result[key] = round((value / HPA_FOR_ONE_INHG), ROUNDED_TO) + return result + + +def visibility_dict_to(d, target_visibility_unit="miles"): + """ + Converts all meter values in a dict to either miles or kms. + + :param d: the dictionary containing metric values + :param target_visibility_unit: either miles or kms, ValueError if neither + :type d: dict + :type target_visibility_unit: str + :returns: a dict with converted values for visibility distance + """ + result = dict() + + if target_visibility_unit == "miles": const = MILE_FOR_ONE_METER + elif target_visibility_unit == "kms": const = KMS_FOR_ONE_METER + else: + e = "Invalid value for target visibility unit" + raise ValueError(e) + + for key, value in d.items(): + if value is None: + continue + result[key] = round(value * const, ROUNDED_TO) + return result + diff --git a/tests/unit/utils/test_measurables.py b/tests/unit/utils/test_measurables.py index 11408369..405f966a 100644 --- a/tests/unit/utils/test_measurables.py +++ b/tests/unit/utils/test_measurables.py @@ -140,3 +140,13 @@ def test_metric_pressure_dict_to_inhg(self): print(result) self.assertEqual(expected, result) + def test_visibility_dict_to(self): + input = {'Paris': 100, 'London': 200, 'Tokyo': None} + cmp_one = {'Paris': .1, 'London': .2} + cmp_two = {'Paris': .06, 'London': .12} + case_one = cmp_one and measurables.visibility_dict_to(input, "kms") + case_two = cmp_two and measurables.visibility_dict_to(input) + self.assertTrue(case_one and case_two) + + def test_visibility_dict_to_value_error(self): + self.assertRaises(ValueError, measurables.visibility_dict_to, {}, "furlongs") From e8cb3235f8d7c56bed1d7db01b7f8c37ceaf7171 Mon Sep 17 00:00:00 2001 From: Darumin Date: Thu, 17 Sep 2020 15:28:52 -0700 Subject: [PATCH 03/36] exposed pressure units in weather.py, with tests --- pyowm/utils/measurables.py | 8 ++++---- pyowm/weatherapi25/weather.py | 14 +++++++++++--- tests/unit/weatherapi25/test_weather.py | 11 +++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pyowm/utils/measurables.py b/pyowm/utils/measurables.py index 77ba921f..0c52e294 100644 --- a/pyowm/utils/measurables.py +++ b/pyowm/utils/measurables.py @@ -212,7 +212,7 @@ def metric_pressure_dict_to_inhg(d): return result -def visibility_dict_to(d, target_visibility_unit="miles"): +def visibility_dict_to(d, target_visibility_unit='miles'): """ Converts all meter values in a dict to either miles or kms. @@ -224,10 +224,10 @@ def visibility_dict_to(d, target_visibility_unit="miles"): """ result = dict() - if target_visibility_unit == "miles": const = MILE_FOR_ONE_METER - elif target_visibility_unit == "kms": const = KMS_FOR_ONE_METER + if target_visibility_unit == 'miles': const = MILE_FOR_ONE_METER + elif target_visibility_unit == 'kms': const = KMS_FOR_ONE_METER else: - e = "Invalid value for target visibility unit" + e = 'Invalid value for target visibility unit' raise ValueError(e) for key, value in d.items(): diff --git a/pyowm/weatherapi25/weather.py b/pyowm/weatherapi25/weather.py index 759e4d57..6ef5015b 100644 --- a/pyowm/weatherapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -88,7 +88,7 @@ def __init__(self, reference_time, sunset_time, sunrise_time, clouds, rain, raise ValueError("'humidity' must be greatear than 0") self.humidity = humidity - self.pressure = pressure + self.press = pressure self.temp = temperature self.status = status self.detailed_status = detailed_status @@ -217,6 +217,15 @@ def temperature(self, unit='kelvin'): return dict(list(converted.items()) + list(not_to_be_converted.items())) + ### + def pressure(self, unit='hPa'): + if unit == 'hPa': + return self.press + elif unit == 'inHg': + return measurables.metric_pressure_dict_to_inhg(self.press) + else: + raise ValueError('Invalid value for target pressure unit') + def weather_icon_url(self): """Returns weather-related icon URL as a string. @@ -494,7 +503,7 @@ def to_dict(self): 'snow': self.snow, 'wind': self.wnd, 'humidity': self.humidity, - 'pressure': self.pressure, + 'pressure': self.press, 'temperature': self.temp, 'status': self.status, 'detailed_status': self.detailed_status, @@ -506,4 +515,3 @@ def to_dict(self): 'heat_index': self.heat_index, 'utc_offset': self.utc_offset, 'uvi': self.uvi} - diff --git a/tests/unit/weatherapi25/test_weather.py b/tests/unit/weatherapi25/test_weather.py index caa9baab..0b2df4ac 100644 --- a/tests/unit/weatherapi25/test_weather.py +++ b/tests/unit/weatherapi25/test_weather.py @@ -34,6 +34,7 @@ class TestWeather(unittest.TestCase): __test_kmh_wind = {'deg': 252.002, 'speed': 3.9600000000000004, 'gust': 7.524} __test_humidity = 57 __test_pressure = {"press": 1030.119, "sea_level": 1038.589, "grnd_level": 1038.773} + __test_inhg_pressure = {'press': 30.42, 'sea_level': 30.67, 'grnd_level': 30.67} __test_temperature = {"temp": 294.199, "temp_kf": -1.899, "temp_max": 296.098, "temp_min": 294.199, "feels_like": 298.0} @@ -530,6 +531,16 @@ def test_returning_different_units_for_wind_values(self): def test_get_wind_fails_with_unknown_units(self): self.assertRaises(ValueError, Weather.wind, self.__test_instance, 'xyz') + def test_returning_different_units_for_pressure_values(self): + result_imperial_inhg = self.__test_instance.pressure(unit='inHg') + result_metric_hpa = self.__test_instance.pressure(unit='hPa') + result_unspecified = self.__test_instance.pressure() + self.assertEqual(result_metric_hpa, result_unspecified) + self.assertEqual(result_imperial_inhg, self.__test_inhg_pressure) + + def test_pressure_fails_with_unknown_units(self): + self.assertRaises(ValueError, Weather.pressure, self.__test_instance, 'xyz') + def test_weather_icon_url(self): expected = ICONS_BASE_URI % self.__test_instance.weather_icon_name result = self.__test_instance.weather_icon_url() From 1dc4f0f40433c09fbd9c947b6e2d4ad043344006 Mon Sep 17 00:00:00 2001 From: Darumin Date: Thu, 17 Sep 2020 16:32:39 -0700 Subject: [PATCH 04/36] refactored visibility behavior and added conversions/test for visibility --- pyowm/utils/measurables.py | 32 ++++++++++++------------- pyowm/weatherapi25/weather.py | 27 ++++++++++++++++++++- tests/unit/utils/test_measurables.py | 22 +++++++++-------- tests/unit/weatherapi25/test_weather.py | 19 +++++++++++++-- 4 files changed, 70 insertions(+), 30 deletions(-) diff --git a/pyowm/utils/measurables.py b/pyowm/utils/measurables.py index 0c52e294..147ad5e8 100644 --- a/pyowm/utils/measurables.py +++ b/pyowm/utils/measurables.py @@ -212,27 +212,25 @@ def metric_pressure_dict_to_inhg(d): return result -def visibility_dict_to(d, target_visibility_unit='miles'): +def visibility_distance_to(v, target_visibility_unit='kilometers'): """ - Converts all meter values in a dict to either miles or kms. + Converts visibility distance (in meters) to kilometers or miles + Defaults to kilometer conversion - :param d: the dictionary containing metric values - :param target_visibility_unit: either miles or kms, ValueError if neither - :type d: dict + :param distance: the value of visibility_distance + :type distance: int + :param target_visibility_unit: the unit of conversion :type target_visibility_unit: str - :returns: a dict with converted values for visibility distance + :returns: a converted value for visibility_distance (float) """ - result = dict() + if v is None: + return v - if target_visibility_unit == 'miles': const = MILE_FOR_ONE_METER - elif target_visibility_unit == 'kms': const = KMS_FOR_ONE_METER + if target_visibility_unit == 'kilometers': + const = KMS_FOR_ONE_METER + elif target_visibility_unit == 'miles': + const = MILE_FOR_ONE_METER else: - e = 'Invalid value for target visibility unit' - raise ValueError(e) - - for key, value in d.items(): - if value is None: - continue - result[key] = round(value * const, ROUNDED_TO) - return result + raise ValueError('Invalid value for target visibility distance unit') + return round(v * const, ROUNDED_TO) diff --git a/pyowm/weatherapi25/weather.py b/pyowm/weatherapi25/weather.py index 6ef5015b..b540ec65 100644 --- a/pyowm/weatherapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -217,8 +217,15 @@ def temperature(self, unit='kelvin'): return dict(list(converted.items()) + list(not_to_be_converted.items())) - ### def pressure(self, unit='hPa'): + """ + Returns a dict with pressure info + :param unit: the unit of measure for the temperature values. May be: + '*hPa' (default), '*inHg*' + :type unit: str + :returns: a dict containing pressure values. + :raises: ValueError when unknown pressure units are provided + """ if unit == 'hPa': return self.press elif unit == 'inHg': @@ -226,6 +233,24 @@ def pressure(self, unit='hPa'): else: raise ValueError('Invalid value for target pressure unit') + def visibility(self, unit='meters'): + """ + Returns a new value for visibility distance with specified unit + :param unit: the unit of measure for the temperature values. May be: + '*meters' (default), '*kilometers*', or '*miles*' + :type unit: str + :returns: a converted visibility distance value (float) + :raises: ValueError when unknown visibility units are provided + """ + if unit == 'meters': + return self.visibility_distance + elif unit == 'kilometers': + return measurables.visibility_distance_to(self.visibility_distance, 'kilometers') + elif unit == 'miles': + return measurables.visibility_distance_to(self.visibility_distance, 'miles') + else: + raise ValueError('Invalid value for target visibility distance unit') + def weather_icon_url(self): """Returns weather-related icon URL as a string. diff --git a/tests/unit/utils/test_measurables.py b/tests/unit/utils/test_measurables.py index 405f966a..f9196072 100644 --- a/tests/unit/utils/test_measurables.py +++ b/tests/unit/utils/test_measurables.py @@ -140,13 +140,15 @@ def test_metric_pressure_dict_to_inhg(self): print(result) self.assertEqual(expected, result) - def test_visibility_dict_to(self): - input = {'Paris': 100, 'London': 200, 'Tokyo': None} - cmp_one = {'Paris': .1, 'London': .2} - cmp_two = {'Paris': .06, 'London': .12} - case_one = cmp_one and measurables.visibility_dict_to(input, "kms") - case_two = cmp_two and measurables.visibility_dict_to(input) - self.assertTrue(case_one and case_two) - - def test_visibility_dict_to_value_error(self): - self.assertRaises(ValueError, measurables.visibility_dict_to, {}, "furlongs") + def test_visibility_distance_to(self): + distances = (100, 200, None) + cmp_kms = (.1, .2, None) + cmp_miles = (.06, .12, None) + case_one, case_two = list(), list() + for distance in distances: + case_one.append(measurables.visibility_distance_to(distance)) + case_two.append(measurables.visibility_distance_to(distance, 'miles')) + self.assertTrue(tuple(case_one) == cmp_kms and tuple(case_two) == cmp_miles) + + def test_visibility_distance_to_fails_with_invalid_unit(self): + self.assertRaises(ValueError, measurables.visibility_distance_to, 10, 'xyz') \ No newline at end of file diff --git a/tests/unit/weatherapi25/test_weather.py b/tests/unit/weatherapi25/test_weather.py index 0b2df4ac..62f7797f 100644 --- a/tests/unit/weatherapi25/test_weather.py +++ b/tests/unit/weatherapi25/test_weather.py @@ -49,6 +49,8 @@ class TestWeather(unittest.TestCase): __test_weather_code = 804 __test_weather_icon_name = "04d" __test_visibility_distance = 1000 + __test_visibility_in_kms = 1 + __test_visibility_in_miles = .62 __test_dewpoint = 300.0 __test_humidex = 298.0 __test_heat_index = 40.0 @@ -535,12 +537,25 @@ def test_returning_different_units_for_pressure_values(self): result_imperial_inhg = self.__test_instance.pressure(unit='inHg') result_metric_hpa = self.__test_instance.pressure(unit='hPa') result_unspecified = self.__test_instance.pressure() - self.assertEqual(result_metric_hpa, result_unspecified) - self.assertEqual(result_imperial_inhg, self.__test_inhg_pressure) + a = result_metric_hpa == result_unspecified + b = result_imperial_inhg == self.__test_inhg_pressure + self.assertTrue(a and b) def test_pressure_fails_with_unknown_units(self): self.assertRaises(ValueError, Weather.pressure, self.__test_instance, 'xyz') + def test_returning_different_units_for_visibility(self): + result_metric_kms = self.__test_instance.visibility(unit='kilometers') + result_imperial_miles = self.__test_instance.visibility(unit='miles') + result_unspecified = self.__test_instance.visibility() + a = self.__test_visibility_distance == result_unspecified + b = self.__test_visibility_in_kms == result_metric_kms + c = self.__test_visibility_in_miles == result_imperial_miles + self.assertTrue(a and b and c) + + def test_visibility_fails_with_unknown_units(self): + self.assertRaises(ValueError, Weather.visibility, self.__test_instance, 'xyz') + def test_weather_icon_url(self): expected = ICONS_BASE_URI % self.__test_instance.weather_icon_name result = self.__test_instance.weather_icon_url() From 33a7662e25f17ba2e1e40d430dd8352262ffeb06 Mon Sep 17 00:00:00 2001 From: Darumin Date: Fri, 18 Sep 2020 20:00:04 -0700 Subject: [PATCH 05/36] updated recipes and utilities usage to include pressure and visibility examples --- sphinx/v3/code-recipes.md | 43 +++++++++++++++++++++++++-- sphinx/v3/utilities-usage-examples.md | 25 ++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 0cc0b2f5..cfd84845 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -402,16 +402,53 @@ rain_dict['3h'] ``` ### Get current pressure on a location -Pressure is similar to rain: you get a dict with keys: `press` (atmospheric pressure on the ground in hPa) and `sea_level` -(on the sea level, if location is on the sea) +Pressure is similar to rain, you get a dict with hPa values and these keys: `press` (atmospheric pressure on the +ground, sea level if [no sea level or ground level data](https://openweathermap.org/weather-data)) `sea_level` +(on the sea level, if location is on the sea) and `grnd_level`. Note that `press` used below refers to the +dict value in ```python from pyowm.owm import OWM owm = OWM('your-api-key') mgr = owm.weather_manager() -pressure_dict = mgr.weather_at_place('Berlin,DE').observation.pressure +pressure_dict = mgr.weather_at_place('Berlin,DE').observation.press pressure_dict['press'] pressure_dict['sea_level'] +pressure_dict['grnd_level'] +``` + +Pressure values are given in the metric hPa, or hectopascals (1 hPa is equivalent to 100 pascals). You can easily +convert these values to inches of mercury, or inHg, which is a unit commonly used in the United States. Similar to above, +we can do: + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.weather_manager() +obs = mgr.weather_at_place('Berlin,DE') + +# the default unit is hPa +pressure_dict_unspecified = obs.weather.pressure() +pressure_dict_in_hg = obs.weather.pressure(unit='inHg') +``` + +### Get current visibility distance on a location +You might want to know how clearly you can see objects in Berlin. This is the visibility distance, an average distance +taken from an Observation object and given in meters. You can also convert this value to kilometers or miles. + +```python +from pyowm.owm import OWM + +owm = OWM('your-api-key') +mgr = owm.weather_manager() +obs = mgr.weather_at_place('Berlin,DE') + +# the default value provided by our call (in meters) +visibility = obs.weather.visibility_distance + +# kilometers is the default conversion unit +visibility_in_kms = obs.weather.visibility() +visibility_in_miles = obs.weather.visibility(unit='miles') ``` ### Get today's sunrise and sunset times for a location diff --git a/sphinx/v3/utilities-usage-examples.md b/sphinx/v3/utilities-usage-examples.md index 41c233f1..d3baafb5 100644 --- a/sphinx/v3/utilities-usage-examples.md +++ b/sphinx/v3/utilities-usage-examples.md @@ -223,6 +223,31 @@ knots_wind_dict = measurables.metric_wind_dict_to_knots(msec_wind_dict) beaufort_wind_dict = measurables.metric_wind_dict_to_beaufort(msec_wind_dict) ``` +### Pressure +OWM gives barometric pressure in hPa values, in a +[dict of three pressure items](https://openweathermap.org/weather-data). You can convert these to inHg, which is a +common unit of measurement in the United States. + +```python +from pyowm.utils import measurables + +hpa_pressure_dict = {'press': 1000, 'sea_level': 1000, 'grnd_level': 1000} +inhg_pressure_dict = measurables.metric_pressure_dict_to_inhg(hpa_pressure_dict) +``` + +### Visibility +A typical API response contains a single visibility distance value. This is described as the average visibility in +meters. You can convert this value (from meters) using the function provided to either kms or miles. + +```python +from pyowm.utils import measurables + +visibility = 1000 + +# the default return value is in kilometers +visibility_kms = measurables.visibility_distance_to(visibility) +visibility_miles = measurables.visibility_distance_to(visibility, 'miles') +``` ## `timestamps` module From 489db190ddfcf52ff84e751e1eb8f5d2b97ded64 Mon Sep 17 00:00:00 2001 From: csparpa Date: Fri, 2 Oct 2020 18:32:18 +0200 Subject: [PATCH 06/36] ported back from master --- pyowm/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyowm/constants.py b/pyowm/constants.py index fd761d31..22753ffb 100644 --- a/pyowm/constants.py +++ b/pyowm/constants.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -PYOWM_VERSION = (3, 1, 0) +PYOWM_VERSION = (3, 1, 1) AGRO_API_VERSION = (1, 0, 0) AIRPOLLUTION_API_VERSION = (3, 0, 0) ALERT_API_VERSION = (3, 0, 0) From a0dda674686ebcc64a088aee7b2c3fe569fdb750 Mon Sep 17 00:00:00 2001 From: roman465 Date: Wed, 7 Oct 2020 02:56:42 +0100 Subject: [PATCH 07/36] Bug Fixed.Parsing city names with 2 or more commas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The length of the 'tokens' variable should be 5. City names like "Southern Nations, Nationalities, and People's Region," "Saint Helena, Ascension, and Tristan da Cunha" and a few others were split by a comma into more than 2 parts, causing the 'tokens' length to be more than expected. The longest name is "Villa Presidente Frei, Ñuñoa, Santiago, Chile" (id 7874740). It has three commas, hence split into 4 parts. --- pyowm/commons/cityidregistry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyowm/commons/cityidregistry.py b/pyowm/commons/cityidregistry.py index 4453e04a..7fb36674 100644 --- a/pyowm/commons/cityidregistry.py +++ b/pyowm/commons/cityidregistry.py @@ -145,10 +145,9 @@ def _filter_matching_lines(self, city_name, country, matching): # the specified matching style for line in lines: tokens = line.split(",") - # sometimes city names have an inner comma... - if len(tokens) == 6: - tokens = [tokens[0]+','+tokens[1], tokens[2], tokens[3], - tokens[4], tokens[5]] + # sometimes city names have one or more inner commas + if len(tokens) > 5: + tokens = [','.join(tokens[:-4]), *tokens[-4:]] # check country if country is not None: if tokens[4] != country: From fb0c01a88113917434436d79870d1553d8b8e6bb Mon Sep 17 00:00:00 2001 From: roman465 Date: Sat, 10 Oct 2020 05:41:35 +0100 Subject: [PATCH 08/36] added unit tests to test locations and ids with commas in names --- tests/unit/commons/test_cityidregistry.py | 58 ++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/unit/commons/test_cityidregistry.py b/tests/unit/commons/test_cityidregistry.py index 6ef5f3d7..60f7398c 100644 --- a/tests/unit/commons/test_cityidregistry.py +++ b/tests/unit/commons/test_cityidregistry.py @@ -30,7 +30,9 @@ class TestCityIDRegistry(unittest.TestCase): Abbeville,4829449,31.57184,-85.250488,US Bologna,2829449,30.57184,-83.250488,IT""" _test_file_contents_with_commas_in_names = """Thalassery,1254780,11.75,75.533333,IN -Thale, Stadt,6550950,51.7528,11.058,DE""" +Thale, Stadt,6550950,51.7528,11.058,DE +Pitcairn,5206361,40.403118,-79.778099,PA +Pitcairn, Henderson, Ducie and Oeno Islands,4030699,-25.066669,-130.100006,PN""" test_filelines = [ 'Londinieres,2997784,49.831871,1.40232,FR\n', @@ -251,6 +253,32 @@ def test_ids_for_with_commas_in_city_names(self): self.assertEqual(1, len(result)) self.assertTrue((6550950, 'Thale, Stadt', 'DE') in result) + result = self._instance.ids_for("Pitcairn, Henderson, Ducie and Oeno Islands") + self.assertEqual(1, len(result)) + self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) + + CityIDRegistry._get_lines = ref_to_original + + def test_ids_for_with_commas_in_city_names_like(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.ids_for("Pitca", matching='like') + self.assertTrue((5206361, 'Pitcairn', 'PA') in result) + self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) + + CityIDRegistry._get_lines = ref_to_original + + def test_ids_for_with_commas_in_city_names_like_country(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.ids_for("Pitc", country='PA',matching='like') + self.assertTrue((5206361, 'Pitcairn', 'PA') in result) + + result = self._instance.ids_for("Ducie and Oeno", country='PN',matching='like') + self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) + CityIDRegistry._get_lines = ref_to_original # tests for locations retrieval @@ -373,6 +401,34 @@ def test_locations_for_with_commas_in_city_names(self): Location('Thale, Stadt', 11.058, 51.7528, 6550950, 'DE'), result[0]) + result = self._instance.locations_for("Pitcairn, Henderson, Ducie and Oeno Islands") + self.assertEqual(1, len(result)) + self._assertLocationsEqual( + Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN'), + result[0]) + + CityIDRegistry._get_lines = ref_to_original + + def test_locations_for_with_commas_in_city_names_like(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.locations_for("Pitcai", matching="like") + self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) + self._assertLocationsEqual(result[1], Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN')) + + CityIDRegistry._get_lines = ref_to_original + + def test_locations_for_with_commas_in_city_names_like_country(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.locations_for("Pitca", country='PA', matching="like") + self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) + + result = self._instance.locations_for("Ducie", country='PN', matching="like") + self._assertLocationsEqual(result[1], Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN')) + CityIDRegistry._get_lines = ref_to_original def test_geopoints_for(self): From 82452e9a7c711c382d239ffa80ef8088002db089 Mon Sep 17 00:00:00 2001 From: Aaron Hertz Date: Sun, 25 Oct 2020 09:49:12 -0400 Subject: [PATCH 09/36] Add support for precipitation_probability --- pyowm/weatherapi25/weather.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pyowm/weatherapi25/weather.py b/pyowm/weatherapi25/weather.py index d66ea17d..e4d766ff 100644 --- a/pyowm/weatherapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -54,6 +54,8 @@ class Weather: :type utc_offset: int or None :param uvi: UV index :type uvi: int, float or None + :param precipitation_probability: Probability of precipitation (forecast only) + :type precipitation_probability: float or None :returns: a *Weather* instance :raises: *ValueError* when negative values are provided for non-negative quantities @@ -63,7 +65,7 @@ def __init__(self, reference_time, sunset_time, sunrise_time, clouds, rain, snow, wind, humidity, pressure, temperature, status, detailed_status, weather_code, weather_icon_name, visibility_distance, dewpoint, humidex, heat_index, - utc_offset=None, uvi=None): + utc_offset=None, uvi=None, precipitation_probability=None): if reference_time < 0: raise ValueError("'reference_time' must be greater than 0") self.ref_time = reference_time @@ -117,6 +119,12 @@ def __init__(self, reference_time, sunset_time, sunrise_time, clouds, rain, raise ValueError("'uvi' must be grater than or equal to 0") self.uvi = uvi + if precipitation_probability is not None and \ + (precipitation_probability < 0.0 or precipitation_probability > 1.0): + raise ValueError("'precipitation_probability' must be between " \ + "0.0 and 1.0") + self.precipitation_probability = precipitation_probability + def reference_time(self, timeformat='unix'): """Returns the GMT time telling when the weather was measured @@ -440,11 +448,15 @@ def from_dict(cls, the_dict): # -- UV index uvi = the_dict.get('uvi', None) + # -- Precipitation probability + precipitation_probability = the_dict.get('pop', None) + return Weather(reference_time, sunset_time, sunrise_time, clouds, rain, snow, wind, humidity, pressure, temperature, status, detailed_status, weather_code, weather_icon_name, visibility_distance, dewpoint, humidex, heat_index, - utc_offset=utc_offset, uvi=uvi) + utc_offset=utc_offset, uvi=uvi, + precipitation_probability=precipitation_probability) @classmethod def from_dict_of_lists(cls, the_dict): @@ -513,4 +525,5 @@ def to_dict(self): 'humidex': self.humidex, 'heat_index': self.heat_index, 'utc_offset': self.utc_offset, - 'uvi': self.uvi} \ No newline at end of file + 'uvi': self.uvi, + 'precipitation_probability': self.precipitation_probability} From 6e07f48729d4eaa525323ec29e868236c8b208c4 Mon Sep 17 00:00:00 2001 From: Aaron Hertz Date: Sun, 25 Oct 2020 10:11:34 -0400 Subject: [PATCH 10/36] Update unit tests --- tests/unit/weatherapi25/test_forecast.py | 11 ++++++--- tests/unit/weatherapi25/test_observation.py | 2 +- tests/unit/weatherapi25/test_weather.py | 27 +++++++++++++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/tests/unit/weatherapi25/test_forecast.py b/tests/unit/weatherapi25/test_forecast.py index b7978eb3..44b5d46d 100644 --- a/tests/unit/weatherapi25/test_forecast.py +++ b/tests/unit/weatherapi25/test_forecast.py @@ -21,14 +21,16 @@ class TestForecast(unittest.TestCase): {"temp": 294.199, "temp_kf": -1.899, "temp_max": 296.098, "temp_min": 294.199 }, - "Clouds", "Overcast clouds", 804, "04d", 1000, 300.0, 298.0, 296.0), + "Clouds", "Overcast clouds", 804, "04d", 1000, 300.0, 298.0, 296.0, + precipitation_probability=0.25), Weather(1378459690, 1378496480, 1378449510, 23, {"all": 10}, {"all": 0}, {"deg": 103.4, "speed": 4.2}, 12, {"press": 1070.119, "sea_level": 1078.589}, {"temp": 297.199, "temp_kf": -1.899, "temp_max": 299.0, "temp_min": 295.6 }, - "Clear", "Sky is clear", 804, "02d", 1000, 300.0, 298.0, 296.0) + "Clear", "Sky is clear", 804, "02d", 1000, 300.0, 298.0, 296.0, + precipitation_probability=0.0) ] __test_n_weathers = len(__test_weathers) __test_instance = Forecast("daily", __test_reception_time, __test_location, @@ -54,7 +56,8 @@ class TestForecast(unittest.TestCase): '"sunset_time": 1378496400, "pressure": {"press": 1030.119,' \ ' "sea_level": 1038.589}, "sunrise_time": 1378449600, ' \ '"heat_index": 296.0, "weather_icon_name": "04d", "wind": ' \ - '{"speed": 1.1, "deg": 252.002}, "utc_offset": null, "uvi": null}, {"status": "Clear", ' \ + '{"speed": 1.1, "deg": 252.002}, "utc_offset": null, "uvi": null, ' \ + '"precipitation_probability": 0.25}, {"status": "Clear", ' \ '"visibility_distance": 1000, "humidity": 12, ' \ '"clouds": 23, "temperature": {"temp_kf": -1.899, ' \ '"temp_max": 299.0, "temp": 297.199, "temp_min": 295.6}, ' \ @@ -65,7 +68,7 @@ class TestForecast(unittest.TestCase): '{"press": 1070.119, "sea_level": 1078.589}, ' \ '"sunrise_time": 1378449510, "heat_index": 296.0, ' \ '"weather_icon_name": "02d", "wind": {"speed": 4.2, ' \ - '"deg": 103.4}, "utc_offset": null, "uvi": null}]}' + '"deg": 103.4}, "utc_offset": null, "uvi": null, "precipitation_probability": 0.0}]}' def test_actualize(self): weathers = [Weather(1378459200, 1378496400, 1378449600, 67, diff --git a/tests/unit/weatherapi25/test_observation.py b/tests/unit/weatherapi25/test_observation.py index 8c8a111b..961d00dd 100644 --- a/tests/unit/weatherapi25/test_observation.py +++ b/tests/unit/weatherapi25/test_observation.py @@ -52,7 +52,7 @@ class TestObservation(unittest.TestCase): '"sunrise_time": 1378449600, "heat_index": 296.0, ' \ '"weather_icon_name": "04d", "wind": ' \ '{"speed": 1.1, "deg": 252.002}, "utc_offset": null, ' \ - '"uvi": null}}' + '"uvi": null, "precipitation_probability": null}}' def test_init_fails_when_reception_time_is_negative(self): self.assertRaises(ValueError, Observation, -1234567, \ diff --git a/tests/unit/weatherapi25/test_weather.py b/tests/unit/weatherapi25/test_weather.py index ed03b594..94a0b067 100644 --- a/tests/unit/weatherapi25/test_weather.py +++ b/tests/unit/weatherapi25/test_weather.py @@ -51,6 +51,7 @@ class TestWeather(unittest.TestCase): __test_dewpoint = 300.0 __test_humidex = 298.0 __test_heat_index = 40.0 + __test_precipitation_probability = 0.5 __test_instance = Weather(__test_reference_time, __test_sunset_time, __test_sunrise_time, __test_clouds, __test_rain, @@ -59,7 +60,8 @@ class TestWeather(unittest.TestCase): __test_status, __test_detailed_status, __test_weather_code, __test_weather_icon_name, __test_visibility_distance, __test_dewpoint, - __test_humidex, __test_heat_index) + __test_humidex, __test_heat_index, + precipitation_probability=__test_precipitation_probability) __bad_json = '{"a": "test", "b": 1.234, "c": [ "hello", "world"] }' __bad_json_2 = '{"list": [{"test":"fake"}] }' @@ -75,7 +77,8 @@ class TestWeather(unittest.TestCase): '{"press": 1030.119, "sea_level": 1038.589, "grnd_level": 1038.773}, ' \ '"sunrise_time": 1378449600, "heat_index": 40.0, ' \ '"weather_icon_name": "04d", "humidity": 57, "wind": ' \ - '{"speed": 1.1, "deg": 252.002, "gust": 2.09}, "utc_offset": null, "uvi": null}' + '{"speed": 1.1, "deg": 252.002, "gust": 2.09}, "utc_offset": null, "uvi": null, ' \ + '"precipitation_probability": 0.5}' def test_init_fails_when_wrong_data_provided(self): self.assertRaises(ValueError, Weather, -9876543210, @@ -138,6 +141,26 @@ def test_init_fails_when_wrong_data_provided(self): self.__test_weather_code, self.__test_weather_icon_name, self.__test_visibility_distance, self.__test_dewpoint, self.__test_humidex, self.__test_heat_index, uvi=-1) + self.assertRaises(ValueError, Weather, self.__test_reference_time, + self.__test_sunset_time, self.__test_sunrise_time, + self.__test_clouds, self.__test_rain, self.__test_snow, + self.__test_wind, self.__test_humidity, + self.__test_pressure, self.__test_temperature, + self.__test_status, self.__test_detailed_status, + self.__test_weather_code, self.__test_weather_icon_name, + self.__test_visibility_distance, self.__test_dewpoint, + self.__test_humidex, self.__test_heat_index, + precipitation_probability=-1.0) + self.assertRaises(ValueError, Weather, self.__test_reference_time, + self.__test_sunset_time, self.__test_sunrise_time, + self.__test_clouds, self.__test_rain, self.__test_snow, + self.__test_wind, self.__test_humidity, + self.__test_pressure, self.__test_temperature, + self.__test_status, self.__test_detailed_status, + self.__test_weather_code, self.__test_weather_icon_name, + self.__test_visibility_distance, self.__test_dewpoint, + self.__test_humidex, self.__test_heat_index, + precipitation_probability=2.0) def test_init_when_wind_is_none(self): instance = Weather(self.__test_reference_time, From 2c3e5149396f06ea8a2cb755ab2ee4f320e5f3d9 Mon Sep 17 00:00:00 2001 From: Aaron Hertz Date: Sun, 25 Oct 2020 10:19:17 -0400 Subject: [PATCH 11/36] Add self to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1a40ff4f..a1096bea 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -5,6 +5,7 @@ Contributors will be shown in alphabetical order Code ---- + * [ahertz](https://github.com/ahertz) * [alechewitt](https://github.com/alechewitt) * [camponez](https://github.com/camponez) * [dev-iks](https://github.com/dev-iks) From 77a92821022c45a9dc185125b12c867142bfb071 Mon Sep 17 00:00:00 2001 From: roman465 Date: Tue, 27 Oct 2020 22:51:05 +0000 Subject: [PATCH 12/36] added matching method startswith and respective unit tests --- pyowm/commons/cityidregistry.py | 36 ++++++---- tests/unit/commons/test_cityidregistry.py | 82 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/pyowm/commons/cityidregistry.py b/pyowm/commons/cityidregistry.py index 7fb36674..75fc5e31 100644 --- a/pyowm/commons/cityidregistry.py +++ b/pyowm/commons/cityidregistry.py @@ -14,7 +14,8 @@ class CityIDRegistry: MATCHINGS = { 'exact': lambda city_name, toponym: city_name == toponym, 'nocase': lambda city_name, toponym: city_name.lower() == toponym.lower(), - 'like': lambda city_name, toponym: city_name.lower() in toponym.lower() + 'like': lambda city_name, toponym: city_name.lower() in toponym.lower(), + 'startswith': lambda city_name, toponym: toponym.lower().startswith(city_name.lower()) } def __init__(self, filepath_regex): @@ -50,10 +51,13 @@ def ids_for(self, city_name, country=None, matching='nocase'): :param country: two character str representing the country where to search for the city. Defaults to `None`, which means: search in all countries. - :param matching: str among `exact` (literal, case-sensitive matching), - `nocase` (literal, case-insensitive matching) and `like` (matches cities - whose name contains as a substring the string fed to the function, no - matter the case). Defaults to `nocase`. + :param matching: str. Default is `nocase`. Possible values: + `exact` - literal, case-sensitive matching, + `nocase` - literal, case-insensitive matching, + `like` - matches cities whose name contains, as a substring, the string + fed to the function, case-insensitive, + `startswith` - matches cities whose names start with the string fed + to the function, case-insensitive. :raises ValueError if the value for `matching` is unknown :return: list of tuples """ @@ -79,10 +83,13 @@ def locations_for(self, city_name, country=None, matching='nocase'): :param country: two character str representing the country where to search for the city. Defaults to `None`, which means: search in all countries. - :param matching: str among `exact` (literal, case-sensitive matching), - `nocase` (literal, case-insensitive matching) and `like` (matches cities - whose name contains as a substring the string fed to the function, no - matter the case). Defaults to `nocase`. + :param matching: str. Default is `nocase`. Possible values: + `exact` - literal, case-sensitive matching, + `nocase` - literal, case-insensitive matching, + `like` - matches cities whose name contains, as a substring, the string + fed to the function, case-insensitive, + `startswith` - matches cities whose names start with the string fed + to the function, case-insensitive. :raises ValueError if the value for `matching` is unknown :return: list of `weatherapi25.location.Location` objects """ @@ -109,10 +116,13 @@ def geopoints_for(self, city_name, country=None, matching='nocase'): :param country: two character str representing the country where to search for the city. Defaults to `None`, which means: search in all countries. - :param matching: str among `exact` (literal, case-sensitive matching), - `nocase` (literal, case-insensitive matching) and `like` (matches cities - whose name contains as a substring the string fed to the function, no - matter the case). Defaults to `nocase`. + :param matching: str. Default is `nocase`. Possible values: + `exact` - literal, case-sensitive matching, + `nocase` - literal, case-insensitive matching, + `like` - matches cities whose name contains, as a substring, the string + fed to the function, case-insensitive, + `startswith` - matches cities whose names start with the string fed + to the function, case-insensitive. :raises ValueError if the value for `matching` is unknown :return: list of `pyowm.utils.geo.Point` objects """ diff --git a/tests/unit/commons/test_cityidregistry.py b/tests/unit/commons/test_cityidregistry.py index 60f7398c..b033fde9 100644 --- a/tests/unit/commons/test_cityidregistry.py +++ b/tests/unit/commons/test_cityidregistry.py @@ -113,6 +113,14 @@ def test_city_name_matches(self): 'test', 'test me', 'like')) self.assertFalse(self._instance._city_name_matches( 'foo', 'bar', 'like')) + self.assertTrue(self._instance._city_name_matches( + 'Me', 'test me', 'like')) + self.assertTrue(self._instance._city_name_matches( + 'Test', 'test me', 'startswith')) + self.assertFalse(self._instance._city_name_matches( + 'me', 'test me', 'startswith')) + self.assertFalse(self._instance._city_name_matches( + 'foo', 'bar', 'startswith')) # tests for IDs retrieval @@ -222,6 +230,23 @@ def test_ids_for_matching_criteria(self): self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) + # startswith + result = self._instance.ids_for("abban", matching='startswith') + self.assertEqual(2, len(result)) + self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) + self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) + + result = self._instance.ids_for("dessus", matching='startswith') + self.assertEqual(0, len(result)) + + result = self._instance.ids_for("abbe", matching='startswith') + self.assertEqual(5, len(result)) + self.assertTrue((3038789, 'Abbeville', 'FR') in result) + self.assertTrue((4568985, 'Abbeville', 'US') in result) + + result = self._instance.ids_for("ville", matching='startswith') + self.assertEqual(0, len(result)) + CityIDRegistry._get_lines = original_get_lines CityIDRegistry._get_all_lines = original_get_all_lines @@ -280,6 +305,28 @@ def test_ids_for_with_commas_in_city_names_like_country(self): self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) CityIDRegistry._get_lines = ref_to_original + + def test_ids_for_with_commas_in_city_names_startswith(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.ids_for("Pitc", matching='startswith') + self.assertEqual(2, len(result)) + self.assertTrue((5206361, 'Pitcairn', 'PA') in result) + self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) + + CityIDRegistry._get_lines = ref_to_original + + def test_ids_for_with_commas_in_city_names_startswith_country(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.ids_for("Pitc", country="PA", matching='startswith') + self.assertEqual(1, len(result)) + self.assertTrue((5206361, 'Pitcairn', 'PA') in result) + self.assertFalse((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) + + CityIDRegistry._get_lines = ref_to_original # tests for locations retrieval @@ -358,6 +405,15 @@ def test_locations_for_matching_criteria(self): self._assertLocationsEqual(expected1, result[0]) self._assertLocationsEqual(expected2, result[1]) + # startswith + result = self._instance.locations_for("abba", matching='startswith') + self.assertEqual(2, len(result)) + self._assertLocationsEqual(expected1, result[0]) + self._assertLocationsEqual(expected2, result[1]) + + result = self._instance.locations_for("bbans", matching='startswith') + self.assertEqual(0, len(result)) + CityIDRegistry._get_lines = original_get_lines CityIDRegistry._get_all_lines = original_get_all_lines @@ -431,6 +487,32 @@ def test_locations_for_with_commas_in_city_names_like_country(self): CityIDRegistry._get_lines = ref_to_original + def test_locations_for_with_commas_in_city_names_startswith(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.locations_for("Pitcai", matching="startswith") + self.assertEqual(2, len(result)) + self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) + self._assertLocationsEqual(result[1], Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN')) + + result = self._instance.locations_for("vil", matching="startswith") + self.assertEqual(0, len(result)) + + CityIDRegistry._get_lines = ref_to_original + + def test_locations_for_with_commas_in_city_names_startswith_country(self): + ref_to_original = CityIDRegistry._get_lines + CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names + + result = self._instance.locations_for("Pit", country='PA', matching="startswith") + self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) + + result = self._instance.locations_for("Ducie", country='PN', matching="startswith") + self.assertEqual(0, len(result)) + + CityIDRegistry._get_lines = ref_to_original + def test_geopoints_for(self): ref_to_original = CityIDRegistry._get_lines CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies From 35754331fe5e10bc7e5d358782d15aa7dae74dad Mon Sep 17 00:00:00 2001 From: csparpa Date: Thu, 29 Oct 2020 19:20:47 +0100 Subject: [PATCH 13/36] this should fix #350 --- Pipfile.lock | 132 +++++++++++++++++++-------------------------------- 1 file changed, 50 insertions(+), 82 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 19c05f91..68df2c37 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -41,7 +41,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "pysocks": { @@ -54,9 +53,6 @@ "version": "==1.7.1" }, "requests": { - "extras": [ - "socks" - ], "hashes": [ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" @@ -66,11 +62,10 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.10" + "version": "==1.25.11" } }, "develop": { @@ -93,7 +88,6 @@ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, "babel": { @@ -101,7 +95,6 @@ "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.0" }, "bleach": { @@ -109,7 +102,6 @@ "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.2.1" }, "certifi": { @@ -169,11 +161,10 @@ }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.3" + "version": "==0.4.4" }, "commonmark": { "hashes": [ @@ -232,31 +223,31 @@ }, "cryptography": { "hashes": [ - "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499", - "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154", - "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6", - "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49", - "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f", - "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396", - "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719", - "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db", - "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70", - "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536", - "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe", - "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba", - "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d", - "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7", - "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490", - "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8", - "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921", - "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118", - "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba", - "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3", - "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc", - "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.1.1" + "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", + "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", + "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", + "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", + "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", + "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", + "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", + "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", + "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", + "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", + "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", + "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", + "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", + "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", + "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", + "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", + "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", + "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", + "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", + "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", + "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", + "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" + ], + "index": "pypi", + "version": "==3.2" }, "distlib": { "hashes": [ @@ -276,7 +267,6 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "filelock": { @@ -291,7 +281,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -299,15 +288,14 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "iniconfig": { "hashes": [ - "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", - "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" ], - "version": "==1.0.1" + "version": "==1.1.1" }, "jeepney": { "hashes": [ @@ -330,7 +318,6 @@ "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" ], - "markers": "python_version >= '3.6'", "version": "==21.4.0" }, "markupsafe": { @@ -369,7 +356,6 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "packaging": { @@ -377,22 +363,20 @@ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "pkginfo": { "hashes": [ - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", + "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" ], - "version": "==1.5.0.1" + "version": "==1.6.1" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "pproxy": { @@ -408,7 +392,6 @@ "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.9.0" }, "pycparser": { @@ -416,23 +399,20 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygments": { "hashes": [ - "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", - "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" + "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", + "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" ], - "markers": "python_version >= '3.5'", - "version": "==2.7.1" + "version": "==2.7.2" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { @@ -452,10 +432,10 @@ }, "readme-renderer": { "hashes": [ - "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", - "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" + "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", + "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" ], - "version": "==26.0" + "version": "==28.0" }, "recommonmark": { "hashes": [ @@ -466,9 +446,6 @@ "version": "==0.6.0" }, "requests": { - "extras": [ - "socks" - ], "hashes": [ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" @@ -503,7 +480,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -533,7 +509,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -541,7 +516,6 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -549,7 +523,6 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -557,7 +530,6 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -565,7 +537,6 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -573,7 +544,6 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { @@ -601,11 +571,10 @@ }, "tqdm": { "hashes": [ - "sha256:2dd75fdb764f673b8187643496fcfbeac38348015b665878e582b152f3391cdb", - "sha256:93b7a6a9129fce904f6df4cf3ae7ff431d779be681a95c3344c26f3e6c09abfa" + "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", + "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.50.0" + "version": "==4.51.0" }, "twine": { "hashes": [ @@ -617,11 +586,10 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.10" + "version": "==1.25.11" }, "virtualenv": { "hashes": [ @@ -639,4 +607,4 @@ "version": "==0.5.1" } } -} +} \ No newline at end of file From ca96b203558d741844be4ed14bd746d241a1e334 Mon Sep 17 00:00:00 2001 From: csparpa Date: Thu, 29 Oct 2020 19:21:42 +0100 Subject: [PATCH 14/36] tries to fix #352 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3729ee3d..98bb3e48 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py37, py38, coverage + py37, py38, py39, coverage skip_missing_interpreters = True From e26bf5db715fd02fceac25adcf752d77ed62f78c Mon Sep 17 00:00:00 2001 From: csparpa Date: Thu, 29 Oct 2020 19:26:28 +0100 Subject: [PATCH 15/36] testing if Travis already integrates Python 3.9 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index bc9435f7..dbcdb40b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python python: - "3.7" - "3.8" + - "3.9-dev" script: tox after_success: - coveralls From 8cdd7503ef7e4d7b27b544fedc090a346ddb1f68 Mon Sep 17 00:00:00 2001 From: csparpa Date: Thu, 29 Oct 2020 19:28:37 +0100 Subject: [PATCH 16/36] forgot a few bits --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index dbcdb40b..e667e792 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,8 @@ jobs: script: bash tests/local_installation_test.sh - <<: *local_installation_test python: "3.8" + - <<: *local_installation_test + python: "3.9-dev" - &coverage stage: "Coverage" @@ -42,6 +44,8 @@ jobs: script: coverage run --rcfile=.coveragerc setup.py test -s tests.unit - <<: *coverage python: "3.8" + - <<: *coverage + python: "3.9-dev" - stage: "Build docs" script: cd sphinx && make clean && make html @@ -52,6 +56,8 @@ jobs: script: bash deploy/deploy_to_pypi.sh - <<: *deploy_to_pypi python: "3.8" + - <<: *deploy_to_pypi + python: "3.9-dev" - &pypi_installation_test stage: "PyPI Installation Test" @@ -60,3 +66,5 @@ jobs: script: bash tests/pypi_installation_test.sh - <<: *pypi_installation_test python: "3.8" + - <<: *pypi_installation_test + python: "3.9-dev" From d515732b2228b575b99e2dea7591d1607d723528 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 29 Nov 2020 16:32:57 +0100 Subject: [PATCH 17/36] this is https://github.com/csparpa/pyowm/pull/353 replicated --- CONTRIBUTORS.md | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a1096bea..1b0f2b8c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -41,6 +41,7 @@ Testing Packaging and Distribution -------------------------- * [Diapente](https://github.com/Diapente) + * [onkelbeh](https://github.com/onkelbeh) * [Simone-Zabberoni](https://github.com/Simone-Zabberoni) Wiki diff --git a/setup.py b/setup.py index 7baf09f3..3c0576a8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ author=__author__, author_email=__author_email__, url=__url__, - packages=find_packages(), + packages=find_packages(exclude=['tests']), long_description="""PyOWM is a client Python wrapper library for OpenWeatherMap web APIs. It allows quick and easy consumption of OWM data from Python applications via a simple object model and in a human-friendly fashion.""", include_package_data=True, From 07b588a14c16a27b2fc2b0270ed4c940e6938363 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 29 Nov 2020 17:01:50 +0100 Subject: [PATCH 18/36] Merge branch 'develop' of https://github.com/Darumin/pyowm into Darumin-develop Also, fixing a breaking change on function pressure() (was an attribute before, had been turned to a function): the function has been renamed to barometric_pressure() # Conflicts: # pyowm/weatherapi25/weather.py --- pyowm/weatherapi25/weather.py | 14 +++++++++----- sphinx/v3/code-recipes.md | 6 +++--- tests/unit/weatherapi25/test_weather.py | 12 ++++++------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pyowm/weatherapi25/weather.py b/pyowm/weatherapi25/weather.py index a5f6b8c2..320f2203 100644 --- a/pyowm/weatherapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -90,7 +90,7 @@ def __init__(self, reference_time, sunset_time, sunrise_time, clouds, rain, raise ValueError("'humidity' must be greatear than 0") self.humidity = humidity - self.press = pressure + self.pressure = pressure self.temp = temperature self.status = status self.detailed_status = detailed_status @@ -225,30 +225,34 @@ def temperature(self, unit='kelvin'): return dict(list(converted.items()) + list(not_to_be_converted.items())) - def pressure(self, unit='hPa'): + def barometric_pressure(self, unit='hPa'): """ Returns a dict with pressure info + :param unit: the unit of measure for the temperature values. May be: '*hPa' (default), '*inHg*' :type unit: str :returns: a dict containing pressure values. :raises: ValueError when unknown pressure units are provided + """ if unit == 'hPa': - return self.press + return self.pressure elif unit == 'inHg': - return measurables.metric_pressure_dict_to_inhg(self.press) + return measurables.metric_pressure_dict_to_inhg(self.pressure) else: raise ValueError('Invalid value for target pressure unit') def visibility(self, unit='meters'): """ Returns a new value for visibility distance with specified unit + :param unit: the unit of measure for the temperature values. May be: '*meters' (default), '*kilometers*', or '*miles*' :type unit: str :returns: a converted visibility distance value (float) :raises: ValueError when unknown visibility units are provided + """ if unit == 'meters': return self.visibility_distance @@ -548,7 +552,7 @@ def to_dict(self): 'snow': self.snow, 'wind': self.wnd, 'humidity': self.humidity, - 'pressure': self.press, + 'pressure': self.pressure, 'temperature': self.temp, 'status': self.status, 'detailed_status': self.detailed_status, diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 391f54a7..bfb58ab9 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -418,7 +418,7 @@ dict value in from pyowm.owm import OWM owm = OWM('your-api-key') mgr = owm.weather_manager() -pressure_dict = mgr.weather_at_place('Berlin,DE').observation.press +pressure_dict = mgr.weather_at_place('Berlin,DE').weather.barometric_pressure() pressure_dict['press'] pressure_dict['sea_level'] pressure_dict['grnd_level'] @@ -435,8 +435,8 @@ mgr = owm.weather_manager() obs = mgr.weather_at_place('Berlin,DE') # the default unit is hPa -pressure_dict_unspecified = obs.weather.pressure() -pressure_dict_in_hg = obs.weather.pressure(unit='inHg') +pressure_dict_unspecified = obs.weather.barometric_pressure() +pressure_dict_in_hg = obs.weather.barometric_pressure(unit='inHg') ``` ### Get current visibility distance on a location diff --git a/tests/unit/weatherapi25/test_weather.py b/tests/unit/weatherapi25/test_weather.py index 7318bac1..f3479f56 100644 --- a/tests/unit/weatherapi25/test_weather.py +++ b/tests/unit/weatherapi25/test_weather.py @@ -556,16 +556,16 @@ def test_returning_different_units_for_wind_values(self): def test_get_wind_fails_with_unknown_units(self): self.assertRaises(ValueError, Weather.wind, self.__test_instance, 'xyz') - def test_returning_different_units_for_pressure_values(self): - result_imperial_inhg = self.__test_instance.pressure(unit='inHg') - result_metric_hpa = self.__test_instance.pressure(unit='hPa') - result_unspecified = self.__test_instance.pressure() + def test_barometric_pressure_returning_different_units_for_pressure_values(self): + result_imperial_inhg = self.__test_instance.barometric_pressure(unit='inHg') + result_metric_hpa = self.__test_instance.barometric_pressure(unit='hPa') + result_unspecified = self.__test_instance.barometric_pressure() a = result_metric_hpa == result_unspecified b = result_imperial_inhg == self.__test_inhg_pressure self.assertTrue(a and b) - def test_pressure_fails_with_unknown_units(self): - self.assertRaises(ValueError, Weather.pressure, self.__test_instance, 'xyz') + def test_barometric_pressure_fails_with_unknown_units(self): + self.assertRaises(ValueError, Weather.barometric_pressure, self.__test_instance, 'xyz') def test_returning_different_units_for_visibility(self): result_metric_kms = self.__test_instance.visibility(unit='kilometers') From f13eb5a36d40d98916c9106ec9b0f1c341c54f7c Mon Sep 17 00:00:00 2001 From: Ira Horecka Date: Tue, 1 Dec 2020 23:37:59 -0800 Subject: [PATCH 19/36] refactor: make program more explicit --- CONTRIBUTORS.md | 1 + pyowm/agroapi10/agro_manager.py | 30 +++++---- pyowm/agroapi10/imagery.py | 10 +-- pyowm/agroapi10/polygon.py | 2 +- pyowm/agroapi10/search.py | 2 +- pyowm/agroapi10/soil.py | 6 +- pyowm/alertapi30/condition.py | 2 +- pyowm/alertapi30/trigger.py | 19 +++--- pyowm/commons/cityidregistry.py | 9 ++- pyowm/commons/http_client.py | 17 ++--- pyowm/stationsapi30/buffer.py | 8 +-- pyowm/stationsapi30/persistence_backend.py | 4 +- pyowm/stationsapi30/station.py | 10 +-- pyowm/stationsapi30/stations_manager.py | 72 +++++++++++----------- pyowm/utils/geo.py | 8 +-- pyowm/utils/measurables.py | 8 +-- pyowm/utils/strings.py | 3 +- pyowm/utils/timestamps.py | 28 ++++----- pyowm/utils/weather.py | 30 ++++----- pyowm/uvindexapi30/uvindex_manager.py | 6 +- pyowm/weatherapi25/forecast.py | 2 +- pyowm/weatherapi25/forecaster.py | 41 +++++------- pyowm/weatherapi25/location.py | 15 +---- pyowm/weatherapi25/observation.py | 7 +-- pyowm/weatherapi25/stationhistory.py | 7 +-- pyowm/weatherapi25/weather.py | 30 ++++----- pyowm/weatherapi25/weather_manager.py | 9 +-- pyowm/weatherapi25/weathercoderegistry.py | 2 +- scripts/fill_entity_template.py | 5 +- scripts/generate_city_id_files.py | 11 +--- 30 files changed, 179 insertions(+), 225 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1a40ff4f..548fb109 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -13,6 +13,7 @@ Code * [edenhaus](https://github.com/edenhaus) * [eumiro](https://github.com/eumiro) * [ggstuart](https://github.com/ggstuart) + * [irahorecka](https://github.com/irahorecka) * [jwmelvin](https://github.com/jwmelvin) * [lardconcepts](https://github.com/lardconcepts) * [liato](https://github.com/liato) diff --git a/pyowm/agroapi10/agro_manager.py b/pyowm/agroapi10/agro_manager.py index 1d9c9870..9b1e350a 100644 --- a/pyowm/agroapi10/agro_manager.py +++ b/pyowm/agroapi10/agro_manager.py @@ -51,12 +51,14 @@ def create_polygon(self, geopolygon, name=None): """ assert geopolygon is not None assert isinstance(geopolygon, GeoPolygon) - data = dict() - data['geo_json'] = { - "type": "Feature", - "properties": {}, - "geometry": geopolygon.to_dict() + data = { + 'geo_json': { + "type": "Feature", + "properties": {}, + "geometry": geopolygon.to_dict(), + } } + if name is not None: data['name'] = name status, payload = self.http_client.post( @@ -144,12 +146,14 @@ def soil_data(self, polygon): params={'appid': self.API_key, 'polyid': polyd}, headers={'Content-Type': 'application/json'}) - the_dict = dict() - the_dict['reference_time'] = data['dt'] - the_dict['surface_temp'] = data['t0'] - the_dict['ten_cm_temp'] = data['t10'] - the_dict['moisture'] = data['moisture'] - the_dict['polygon_id'] = polyd + the_dict = { + 'reference_time': data['dt'], + 'surface_temp': data['t0'], + 'ten_cm_temp': data['t10'], + 'moisture': data['moisture'], + 'polygon_id': polyd, + } + return Soil.from_dict(the_dict) # Satellite Imagery subset methods @@ -271,7 +275,7 @@ def download_satellite_image(self, metaimage, x=None, y=None, zoom=None, palette params = dict(paletteid=palette) else: palette = PaletteEnum.GREEN - params = dict() + params = {} # polygon PNG if isinstance(metaimage, MetaPNGImage): prepared_url = metaimage.url @@ -309,7 +313,7 @@ def stats_for_satellite_image(self, metaimage): :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype :return: dict """ - if metaimage.preset != PresetEnum.EVI and metaimage.preset != PresetEnum.NDVI: + if metaimage.preset not in [PresetEnum.EVI, PresetEnum.NDVI]: raise ValueError("Unsupported image preset: should be EVI or NDVI") if metaimage.stats_url is None: raise ValueError("URL for image statistics is not defined") diff --git a/pyowm/agroapi10/imagery.py b/pyowm/agroapi10/imagery.py index 09ed3b4f..32623c48 100644 --- a/pyowm/agroapi10/imagery.py +++ b/pyowm/agroapi10/imagery.py @@ -47,16 +47,16 @@ def __init__(self, url, preset, satellite_name, acquisition_time, assert isinstance(acquisition_time, int) assert acquisition_time >= 0, 'acquisition_time cannot be negative' self._acquisition_time = acquisition_time - assert isinstance(valid_data_percentage, float) or isinstance(valid_data_percentage, int) + assert isinstance(valid_data_percentage, (float, int)) assert valid_data_percentage >= 0., 'valid_data_percentage cannot be negative' self.valid_data_percentage = valid_data_percentage - assert isinstance(cloud_coverage_percentage, float) or isinstance(cloud_coverage_percentage, int) + assert isinstance(cloud_coverage_percentage, (float, int)) assert cloud_coverage_percentage >= 0., 'cloud_coverage_percentage cannot be negative' self.cloud_coverage_percentage = cloud_coverage_percentage - assert isinstance(sun_azimuth, float) or isinstance(sun_azimuth, int) + assert isinstance(sun_azimuth, (float, int)) assert sun_azimuth >= 0. and sun_azimuth <= 360., 'sun_azimuth must be between 0 and 360 degrees' self.sun_azimuth = sun_azimuth - assert isinstance(sun_elevation, float) or isinstance(sun_elevation, int) + assert isinstance(sun_elevation, (float, int)) assert sun_elevation >= 0. and sun_elevation <= 90., 'sun_elevation must be between 0 and 90 degrees' self.sun_elevation = sun_elevation self.polygon_id = polygon_id @@ -122,7 +122,7 @@ class SatelliteImage: def __init__(self, metadata, data, downloaded_on=None, palette=None): assert isinstance(metadata, MetaImage) self.metadata = metadata - assert isinstance(data, Image) or isinstance(data, Tile) + assert isinstance(data, (Image, Tile)) self.data = data if downloaded_on is not None: assert isinstance(downloaded_on, int) diff --git a/pyowm/agroapi10/polygon.py b/pyowm/agroapi10/polygon.py index 4b2fff27..c4bfb686 100644 --- a/pyowm/agroapi10/polygon.py +++ b/pyowm/agroapi10/polygon.py @@ -35,7 +35,7 @@ def __init__(self, id, name=None, geopolygon=None, center=None, area=None, user_ if center is not None: assert isinstance(center, GeoPoint), 'Polygon center must be a valid geopoint type' if area is not None: - assert isinstance(area, float) or isinstance(area, int), 'Area must be a numeric type' + assert isinstance(area, (float, int)), 'Area must be a numeric type' assert area >= 0, 'Area must not be negative' self.id = id self.name = name diff --git a/pyowm/agroapi10/search.py b/pyowm/agroapi10/search.py index b117156a..57c77af2 100644 --- a/pyowm/agroapi10/search.py +++ b/pyowm/agroapi10/search.py @@ -37,7 +37,7 @@ def __init__(self, polygon_id, list_of_dict, query_timestamp): self.query_timestamp = query_timestamp # parse raw data - result = list() + result = [] for the_dict in list_of_dict: # common metadata acquisition_time = the_dict.get('dt', None) diff --git a/pyowm/agroapi10/soil.py b/pyowm/agroapi10/soil.py index dc6971fd..3fd55a56 100644 --- a/pyowm/agroapi10/soil.py +++ b/pyowm/agroapi10/soil.py @@ -30,13 +30,13 @@ def __init__(self, reference_time, surface_temp, ten_cm_temp, moisture, polygon_ raise ValueError("reference_time must be greater than 0") self._reference_time = reference_time assert surface_temp is not None - assert isinstance(surface_temp, float) or isinstance(surface_temp, int), 'surface_temp must be a number' + assert isinstance(surface_temp, (float, int)), 'surface_temp must be a number' self._surface_temp = surface_temp assert ten_cm_temp is not None - assert isinstance(ten_cm_temp, float) or isinstance(ten_cm_temp, int), 'ten_cm_temp must be a number' + assert isinstance(ten_cm_temp, (float, int)), 'ten_cm_temp must be a number' self._ten_cm_temp = ten_cm_temp assert moisture is not None - assert isinstance(moisture, float) or isinstance(moisture, int), 'moisture must be a number' + assert isinstance(moisture, (float, int)), 'moisture must be a number' if moisture < 0.: raise ValueError("moisture must be greater than 0") self.moisture = moisture diff --git a/pyowm/alertapi30/condition.py b/pyowm/alertapi30/condition.py index e1f3425a..535586f4 100644 --- a/pyowm/alertapi30/condition.py +++ b/pyowm/alertapi30/condition.py @@ -33,7 +33,7 @@ def __init__(self, weather_param, operator, amount, id=None): self.operator = operator assert amount is not None - assert isinstance(amount, int) or isinstance(amount, float) + assert isinstance(amount, (int, float)) self.amount = amount self.id = id diff --git a/pyowm/alertapi30/trigger.py b/pyowm/alertapi30/trigger.py index 3cf2e97f..feb824bc 100644 --- a/pyowm/alertapi30/trigger.py +++ b/pyowm/alertapi30/trigger.py @@ -56,7 +56,7 @@ def __init__(self, start_after_millis, end_after_millis, conditions, area, alert raise ValueError('The area for a trigger must contain at least one geoJSON type: you provided none') self.area = area if alerts is None or len(alerts) == 0: - self.alerts = list() + self.alerts = [] else: self.alerts = alerts if alert_channels is None or len(alert_channels) == 0: @@ -91,11 +91,7 @@ def get_alerts_since(self, timestamp): :return: list of `Alert` instances """ unix_timestamp = formatting.to_UNIXtime(timestamp) - result = [] - for alert in self.alerts: - if alert.last_update >= unix_timestamp: - result.append(alert) - return result + return [alert for alert in self.alerts if alert.last_update >= unix_timestamp] def get_alerts_on(self, weather_param): """ @@ -145,7 +141,7 @@ def from_dict(cls, the_dict): alert_id = key alert_data = alerts_dict[alert_id] alert_last_update = alert_data['last_update'] - alert_met_conds = list() + alert_met_conds = [] for c in alert_data['conditions']: if isinstance(c['current_value'], int): cv = c['current_value'] @@ -165,9 +161,14 @@ def from_dict(cls, the_dict): alert_channels = None # defaulting except ValueError as e: - raise pyowm.commons.exceptions.ParseAPIResponseError('Impossible to parse JSON: %s' % e) + raise pyowm.commons.exceptions.ParseAPIResponseError( + 'Impossible to parse JSON: %s' % e + ) + except KeyError as e: - raise pyowm.commons.exceptions.ParseAPIResponseError('Impossible to parse JSON: %s' % e) + raise pyowm.commons.exceptions.ParseAPIResponseError( + 'Impossible to parse JSON: %s' % e + ) return Trigger(start, end, conditions, area=area, alerts=alerts, alert_channels=alert_channels, id=trigger_id) diff --git a/pyowm/commons/cityidregistry.py b/pyowm/commons/cityidregistry.py index 4453e04a..d259bcf9 100644 --- a/pyowm/commons/cityidregistry.py +++ b/pyowm/commons/cityidregistry.py @@ -131,7 +131,7 @@ def _filter_matching_lines(self, city_name, country, matching): :param matching: str :return: list of lists """ - result = list() + result = [] # find the right file to scan and extract its lines. Upon "like" # matchings, just read all files @@ -150,9 +150,8 @@ def _filter_matching_lines(self, city_name, country, matching): tokens = [tokens[0]+','+tokens[1], tokens[2], tokens[3], tokens[4], tokens[5]] # check country - if country is not None: - if tokens[4] != country: - continue + if country is not None and tokens[4] != country: + continue # check city_name if self._city_name_matches(city_name, tokens[0], matching): @@ -193,7 +192,7 @@ def _get_lines(self, filename): return lines def _get_all_lines(self): - all_lines = list() + all_lines = [] for city_name in ['a', 'g', 'm', 's']: # all available city ID files filename = self._assess_subfile_from(city_name) all_lines.extend(self._get_lines(filename)) diff --git a/pyowm/commons/http_client.py b/pyowm/commons/http_client.py index 9162d4e2..7ba435b7 100644 --- a/pyowm/commons/http_client.py +++ b/pyowm/commons/http_client.py @@ -29,18 +29,15 @@ def __init__(self, root_uri_token, api_key, config, has_subdomains=True): self.subdomain = None self.proxies = None self.path = None - self.params = dict() - self.headers = dict() + self.params = {} + self.headers = {} self._set_schema() self._set_subdomain() self._set_proxies() def _set_schema(self): use_ssl = self.config['connection']['use_ssl'] - if use_ssl: - self.schema = 'https' - else: - self.schema = 'http' + self.schema = 'https' if use_ssl else 'http' def _set_subdomain(self): if self.has_subdomains: @@ -51,7 +48,7 @@ def _set_proxies(self): if self.config['connection']['use_proxy']: self.proxies = self.config['proxies'] else: - self.proxies = dict() + self.proxies = {} def with_path(self, path_uri_token): assert isinstance(path_uri_token, str) @@ -280,16 +277,14 @@ def delete(self, path, params=None, data=None, headers=None): def check_status_code(cls, status_code, payload): if status_code < 400: return - if status_code == 400: + if status_code == 400 or status_code not in [401, 404, 502]: raise exceptions.APIRequestError(payload) elif status_code == 401: raise exceptions.UnauthorizedError('Invalid API Key provided') elif status_code == 404: raise exceptions.NotFoundError('Unable to find the resource') - elif status_code == 502: - raise exceptions.BadGatewayError('Unable to contact the upstream server') else: - raise exceptions.APIRequestError(payload) + raise exceptions.BadGatewayError('Unable to contact the upstream server') def __repr__(self): return "<%s.%s - root: %s>" % (__name__, self.__class__.__name__, self.root_uri) diff --git a/pyowm/stationsapi30/buffer.py b/pyowm/stationsapi30/buffer.py index e9e1b94d..34e9d9ad 100644 --- a/pyowm/stationsapi30/buffer.py +++ b/pyowm/stationsapi30/buffer.py @@ -17,7 +17,7 @@ def __init__(self, station_id): assert station_id is not None self.station_id = station_id self.created_at = timestamps.now(timeformat='unix') - self.measurements = list() + self.measurements = [] def creation_time(self, timeformat='unix'): """Returns the UTC time of creation of this aggregated measurement @@ -70,7 +70,7 @@ def empty(self): Drops all measurements of this buffer instance """ - self.measurements = list() + self.measurements = [] def sort_chronologically(self): """ @@ -90,10 +90,10 @@ def __len__(self): return len(self.measurements) def __iter__(self): - return (m for m in self.measurements) + return iter(self.measurements) def __add__(self, other): - assert all([i.station_id == self.station_id for i in other]) + assert all(i.station_id == self.station_id for i in other) result = copy.deepcopy(self) for m in other.measurements: result.append(m) diff --git a/pyowm/stationsapi30/persistence_backend.py b/pyowm/stationsapi30/persistence_backend.py index 5fc8aea2..a2186710 100644 --- a/pyowm/stationsapi30/persistence_backend.py +++ b/pyowm/stationsapi30/persistence_backend.py @@ -76,8 +76,6 @@ def load_to_buffer(self): return result def persist_buffer(self, buffer): - data = list() with open(self._file_path, 'w') as f: - for msmt in buffer: - data.append(msmt.to_JSON()) + data = [msmt.to_JSON() for msmt in buffer] f.write('[%s]' % ','.join(data)) diff --git a/pyowm/stationsapi30/station.py b/pyowm/stationsapi30/station.py index 90ee1aed..fc220e55 100644 --- a/pyowm/stationsapi30/station.py +++ b/pyowm/stationsapi30/station.py @@ -43,9 +43,8 @@ def __init__(self, id, created_at, updated_at, external_id, name, self._lon = float(lon) if lat < -90.0 or lat > 90.0: raise ValueError("'lat' value must be between -90 and 90") - if alt is not None: - if alt < 0.0: - raise ValueError("'alt' value must not be negative") + if alt is not None and alt < 0.0: + raise ValueError("'alt' value must not be negative") self.id = id self.created_at = created_at if self.created_at is not None: @@ -72,10 +71,7 @@ def _format_micros(self, datestring): else: return datestring + '.000000Z' else: - if len(parts[-1]) > 6: - micros = parts[-1][:6] - else: - micros = parts[-1] + micros = parts[-1][:6] if len(parts[-1]) > 6 else parts[-1] return '.'.join( parts[:-1] + ['{:06d}'.format(int(micros))]) + 'Z' diff --git a/pyowm/stationsapi30/stations_manager.py b/pyowm/stationsapi30/stations_manager.py index a45c18b5..f585326d 100644 --- a/pyowm/stationsapi30/stations_manager.py +++ b/pyowm/stationsapi30/stations_manager.py @@ -88,9 +88,8 @@ def create_station(self, external_id, name, lat, lon, alt=None): raise ValueError("'lon' value must be between -180 and 180") if lat < -90.0 or lat > 90.0: raise ValueError("'lat' value must be between -90 and 90") - if alt is not None: - if alt < 0.0: - raise ValueError("'alt' value must not be negative") + if alt is not None and alt < 0.0: + raise ValueError("'alt' value must not be negative") status, payload = self.http_client.post( STATIONS_URI, params={'appid': self.API_key}, @@ -163,7 +162,7 @@ def send_measurements(self, list_of_measurements): :returns: `None` if creation is successful, an exception otherwise """ assert list_of_measurements is not None - assert all([m.station_id is not None for m in list_of_measurements]) + assert all(m.station_id is not None for m in list_of_measurements) msmts = [self._structure_dict(m) for m in list_of_measurements] status, _ = self.http_client.post( MEASUREMENTS_URI, @@ -235,37 +234,40 @@ def send_buffer(self, buffer): def _structure_dict(self, measurement): d = measurement.to_dict() - item = dict() - item['station_id'] = d['station_id'] - item['dt'] = d['timestamp'] - item['temperature'] = d['temperature'] - item['wind_speed'] = d['wind_speed'] - item['wind_gust'] = d['wind_gust'] - item['wind_deg'] = d['wind_deg'] - item['pressure'] = d['pressure'] - item['humidity'] = d['humidity'] - item['rain_1h'] = d['rain_1h'] - item['rain_6h'] = d['rain_6h'] - item['rain_24h'] = d['rain_24h'] - item['snow_1h'] = d['snow_1h'] - item['snow_6h'] = d['snow_6h'] - item['snow_24h'] = d['snow_24h'] - item['dew_point'] = d['dew_point'] - item['humidex'] = d['humidex'] - item['heat_index'] = d['heat_index'] - item['visibility_distance'] = d['visibility_distance'] - item['visibility_prefix'] = d['visibility_prefix'] - item['clouds'] = [dict(distance=d['clouds_distance']), - dict(condition=d['clouds_condition']), - dict(cumulus=d['clouds_cumulus'])] - item['weather'] = [ - dict(precipitation=d['weather_precipitation']), - dict(descriptor=d['weather_descriptor']), - dict(intensity=d['weather_intensity']), - dict(proximity=d['weather_proximity']), - dict(obscuration=d['weather_obscuration']), - dict(other=d['weather_other'])] - return item + return { + 'station_id': d['station_id'], + 'dt': d['timestamp'], + 'temperature': d['temperature'], + 'wind_speed': d['wind_speed'], + 'wind_gust': d['wind_gust'], + 'wind_deg': d['wind_deg'], + 'pressure': d['pressure'], + 'humidity': d['humidity'], + 'rain_1h': d['rain_1h'], + 'rain_6h': d['rain_6h'], + 'rain_24h': d['rain_24h'], + 'snow_1h': d['snow_1h'], + 'snow_6h': d['snow_6h'], + 'snow_24h': d['snow_24h'], + 'dew_point': d['dew_point'], + 'humidex': d['humidex'], + 'heat_index': d['heat_index'], + 'visibility_distance': d['visibility_distance'], + 'visibility_prefix': d['visibility_prefix'], + 'clouds': [ + dict(distance=d['clouds_distance']), + dict(condition=d['clouds_condition']), + dict(cumulus=d['clouds_cumulus']), + ], + 'weather': [ + dict(precipitation=d['weather_precipitation']), + dict(descriptor=d['weather_descriptor']), + dict(intensity=d['weather_intensity']), + dict(proximity=d['weather_proximity']), + dict(obscuration=d['weather_obscuration']), + dict(other=d['weather_other']), + ], + } def __repr__(self): return '<%s.%s>' % (__name__, self.__class__.__name__) \ No newline at end of file diff --git a/pyowm/utils/geo.py b/pyowm/utils/geo.py index 5e83e924..255326f2 100644 --- a/pyowm/utils/geo.py +++ b/pyowm/utils/geo.py @@ -103,7 +103,7 @@ def bounding_square_polygon(self, inscribed_circle_radius_km=10.0): :type inscribed_circle_radius_km: int or float :return: a `pyowm.utils.geo.Polygon` instance """ - assert isinstance(inscribed_circle_radius_km, int) or isinstance(inscribed_circle_radius_km, float) + assert isinstance(inscribed_circle_radius_km, (int, float)) assert inscribed_circle_radius_km > 0., 'Radius must be greater than zero' # turn metric distance to radians on the approximated local sphere @@ -248,7 +248,7 @@ def __init__(self, list_of_lists): if not list_of_lists: raise ValueError("A Polygon cannot be empty") first, last = list_of_lists[0][0], list_of_lists[0][-1] - if not first == last: + if first != last: raise ValueError("The start and end point of Polygon must coincide") self._geom = geojson.Polygon(list_of_lists) @@ -292,9 +292,7 @@ def from_points(cls, list_of_lists): """ result = [] for l in list_of_lists: - curve = [] - for point in l: - curve.append((point.lon, point.lat)) + curve = [(point.lon, point.lat) for point in l] result.append(curve) return Polygon(result) diff --git a/pyowm/utils/measurables.py b/pyowm/utils/measurables.py index ec4421ac..917cab45 100644 --- a/pyowm/utils/measurables.py +++ b/pyowm/utils/measurables.py @@ -85,7 +85,7 @@ def metric_wind_dict_to_imperial(d): to miles/hour """ - result = dict() + result = {} for key, value in d.items(): if key != 'deg': # do not convert wind degree result[key] = value * MILES_PER_HOUR_FOR_ONE_METER_PER_SEC @@ -105,7 +105,7 @@ def metric_wind_dict_to_km_h(d): to km/hour """ - result = dict() + result = {} for key, value in d.items(): if key != 'deg': # do not convert wind degree result[key] = value * KM_PER_HOUR_FOR_ONE_METER_PER_SEC @@ -125,7 +125,7 @@ def metric_wind_dict_to_knots(d): to km/hour """ - result = dict() + result = {} for key, value in d.items(): if key != 'deg': # do not convert wind degree result[key] = value * KNOTS_FOR_ONE_METER_PER_SEC @@ -147,7 +147,7 @@ def metric_wind_dict_to_beaufort(d): to Beaufort level """ - result = dict() + result = {} for key, value in d.items(): if key != 'deg': # do not convert wind degree if value <= 0.2: diff --git a/pyowm/utils/strings.py b/pyowm/utils/strings.py index cde250d3..7b37ad79 100644 --- a/pyowm/utils/strings.py +++ b/pyowm/utils/strings.py @@ -41,5 +41,4 @@ def class_from_dotted_path(dotted_path): assert isinstance(dotted_path, str), 'A string must be provided' tokens = dotted_path.split('.') modpath, class_name = '.'.join(tokens[:-1]), tokens[-1] - klass = getattr(importlib.import_module(modpath), class_name) - return klass + return getattr(importlib.import_module(modpath), class_name) diff --git a/pyowm/utils/timestamps.py b/pyowm/utils/timestamps.py index a9decef4..1fab8734 100644 --- a/pyowm/utils/timestamps.py +++ b/pyowm/utils/timestamps.py @@ -217,37 +217,33 @@ def next_year(date=None): def _timedelta_hours(offset, date=None): if date is None: return datetime.now(timezone.utc) + timedelta(hours=offset) - else: - assert isinstance(date, datetime), __name__ + \ - ": 'date' must be a datetime.datetime object" - return date + timedelta(hours=offset) + assert isinstance(date, datetime), __name__ + \ + ": 'date' must be a datetime.datetime object" + return date + timedelta(hours=offset) def _timedelta_days(offset, date=None): if date is None: return datetime.now(timezone.utc) + timedelta(days=offset) - else: - assert isinstance(date, datetime), __name__ + \ - ": 'date' must be a datetime.datetime object" - return date + timedelta(days=offset) + assert isinstance(date, datetime), __name__ + \ + ": 'date' must be a datetime.datetime object" + return date + timedelta(days=offset) def _timedelta_months(offset, date=None): if date is None: return datetime.now(timezone.utc) + timedelta(days=offset * 30) - else: - assert isinstance(date, datetime), __name__ + \ - ": 'date' must be a datetime.datetime object" - return date + timedelta(days=offset * 30) + assert isinstance(date, datetime), __name__ + \ + ": 'date' must be a datetime.datetime object" + return date + timedelta(days=offset * 30) def _timedelta_years(offset, date=None): if date is None: return datetime.now(timezone.utc) + timedelta(days=offset * 365) - else: - assert isinstance(date, datetime), __name__ + \ - ": 'date' must be a datetime.datetime object" - return date + timedelta(days=offset * 365) + assert isinstance(date, datetime), __name__ + \ + ": 'date' must be a datetime.datetime object" + return date + timedelta(days=offset * 365) def millis_offset_between_epochs(reference_epoch, target_epoch): diff --git a/pyowm/utils/weather.py b/pyowm/utils/weather.py index 2405bf97..0a2da330 100644 --- a/pyowm/utils/weather.py +++ b/pyowm/utils/weather.py @@ -39,10 +39,10 @@ def any_status_is(weather_list, status, weather_code_registry): :returns: ``True`` if the check is positive, ``False`` otherwise """ - for weather in weather_list: - if status_is(weather, status, weather_code_registry): - return True - return False + return any( + status_is(weather, status, weather_code_registry) + for weather in weather_list + ) def filter_by_status(weather_list, status, weather_code_registry): @@ -60,11 +60,11 @@ def filter_by_status(weather_list, status, weather_code_registry): :returns: ``True`` if the check is positive, ``False`` otherwise """ - result = [] - for weather in weather_list: - if status_is(weather, status, weather_code_registry): - result.append(weather) - return result + return [ + weather + for weather in weather_list + if status_is(weather, status, weather_code_registry) + ] def is_in_coverage(unixtime, weathers_list): @@ -83,14 +83,10 @@ def is_in_coverage(unixtime, weathers_list): """ if not weathers_list: return False - else: - min_of_coverage = min([weather.reference_time() \ - for weather in weathers_list]) - max_of_coverage = max([weather.reference_time() \ - for weather in weathers_list]) - if unixtime < min_of_coverage or unixtime > max_of_coverage: - return False - return True + min_of_coverage = min(weather.reference_time() for weather in weathers_list) + max_of_coverage = max([weather.reference_time() \ + for weather in weathers_list]) + return unixtime >= min_of_coverage and unixtime <= max_of_coverage def find_closest_weather(weathers_list, unixtime): diff --git a/pyowm/uvindexapi30/uvindex_manager.py b/pyowm/uvindexapi30/uvindex_manager.py index 7c852ee4..7998f4c3 100644 --- a/pyowm/uvindexapi30/uvindex_manager.py +++ b/pyowm/uvindexapi30/uvindex_manager.py @@ -72,8 +72,7 @@ def uvindex_forecast_around_coords(self, lat, lon): geo.assert_is_lat(lat) params = {'lon': lon, 'lat': lat} json_data = self.uv_client.get_uvi_forecast(params) - uvindex_list = [uvindex.UVIndex.from_dict(item) for item in json_data] - return uvindex_list + return [uvindex.UVIndex.from_dict(item) for item in json_data] def uvindex_history_around_coords(self, lat, lon, start, end=None): """ @@ -107,8 +106,7 @@ def uvindex_history_around_coords(self, lat, lon, start, end=None): end = formatting.timeformat(end, 'unix') params = {'lon': lon, 'lat': lat, 'start': start, 'end': end} json_data = self.uv_client.get_uvi_history(params) - uvindex_list = [uvindex.UVIndex.from_dict(item) for item in json_data] - return uvindex_list + return [uvindex.UVIndex.from_dict(item) for item in json_data] def __repr__(self): return '<%s.%s>' % (__name__, self.__class__.__name__) \ No newline at end of file diff --git a/pyowm/weatherapi25/forecast.py b/pyowm/weatherapi25/forecast.py index 221af34b..18dd2d05 100644 --- a/pyowm/weatherapi25/forecast.py +++ b/pyowm/weatherapi25/forecast.py @@ -139,7 +139,7 @@ def __len__(self): return len(self.weathers) def __iter__(self): - return (w for w in self.weathers) + return iter(self.weathers) def __repr__(self): return "<%s.%s - reception_time=%s, interval=%s>" % (__name__, \ diff --git a/pyowm/weatherapi25/forecaster.py b/pyowm/weatherapi25/forecaster.py index da8f3199..e0362789 100644 --- a/pyowm/weatherapi25/forecaster.py +++ b/pyowm/weatherapi25/forecaster.py @@ -37,8 +37,7 @@ def when_starts(self, timeformat='unix'): :raises: *ValueError* when invalid time format values are provided """ - start_coverage = min([item.reference_time() \ - for item in self.forecast]) + start_coverage = min(item.reference_time() for item in self.forecast) return formatting.timeformat(start_coverage, timeformat) def when_ends(self, timeformat='unix'): @@ -55,8 +54,7 @@ def when_ends(self, timeformat='unix'): :raises: *ValueError* when invalid time format values are provided """ - end_coverage = max([item.reference_time() - for item in self.forecast]) + end_coverage = max(item.reference_time() for item in self.forecast) return formatting.timeformat(end_coverage, timeformat) def will_have_rain(self): @@ -382,10 +380,9 @@ def most_hot(self): hottest = None for weather in self.forecast.weathers: d = weather.temperature() - if 'temp_max' in d: - if d['temp_max'] > maxtemp: - maxtemp = d['temp_max'] - hottest = weather + if 'temp_max' in d and d['temp_max'] > maxtemp: + maxtemp = d['temp_max'] + hottest = weather return hottest def most_cold(self): @@ -401,10 +398,9 @@ def most_cold(self): coldest = None for weather in self.forecast.weathers: d = weather.temperature() - if 'temp_min' in d: - if d['temp_min'] < mintemp: - mintemp = d['temp_min'] - coldest = weather + if 'temp_min' in d and d['temp_min'] < mintemp: + mintemp = d['temp_min'] + coldest = weather return coldest def most_humid(self): @@ -437,10 +433,9 @@ def most_rainy(self): most_rainy = None for weather in self.forecast.weathers: d = weather.rain - if 'all' in d: - if d['all'] > max_rain: - max_rain = d['all'] - most_rainy = weather + if 'all' in d and d['all'] > max_rain: + max_rain = d['all'] + most_rainy = weather return most_rainy def most_snowy(self): @@ -456,10 +451,9 @@ def most_snowy(self): most_snowy = None for weather in self.forecast.weathers: d = weather.snow - if 'all' in d: - if d['all'] > max_snow: - max_snow = d['all'] - most_snowy = weather + if 'all' in d and d['all'] > max_snow: + max_snow = d['all'] + most_snowy = weather return most_snowy def most_windy(self): @@ -475,10 +469,9 @@ def most_windy(self): most_windy = None for weather in self.forecast.weathers: d = weather.wind() - if 'speed' in d: - if d['speed'] > max_wind_speed: - max_wind_speed = d['speed'] - most_windy = weather + if 'speed' in d and d['speed'] > max_wind_speed: + max_wind_speed = d['speed'] + most_windy = weather return most_windy def __repr__(self): diff --git a/pyowm/weatherapi25/location.py b/pyowm/weatherapi25/location.py index 0f4a076d..e2570f65 100644 --- a/pyowm/weatherapi25/location.py +++ b/pyowm/weatherapi25/location.py @@ -69,18 +69,9 @@ def from_dict(cls, the_dict): country = None if 'sys' in the_dict and 'country' in the_dict['sys']: country = the_dict['sys']['country'] - if 'city' in the_dict: - data = the_dict['city'] - else: - data = the_dict - if 'name' in data: - name = data['name'] - else: - name = None - if 'id' in data: - ID = int(data['id']) - else: - ID = None + data = the_dict['city'] if 'city' in the_dict else the_dict + name = data['name'] if 'name' in data else None + ID = int(data['id']) if 'id' in data else None if 'coord' in data: lon = data['coord'].get('lon', 0.0) lat = data['coord'].get('lat', 0.0) diff --git a/pyowm/weatherapi25/observation.py b/pyowm/weatherapi25/observation.py index 8631ed39..c6b1414f 100644 --- a/pyowm/weatherapi25/observation.py +++ b/pyowm/weatherapi25/observation.py @@ -71,12 +71,11 @@ def from_dict(cls, the_dict): # supposed to be deprecated as soon as the API fully adopts HTTP for # conveying errors to the clients if 'message' in the_dict and 'cod' in the_dict: - if the_dict['cod'] == "404": - print("OWM API: observation data not available") - return None - else: + if the_dict['cod'] != "404": raise exceptions.APIResponseError( "OWM API: error - response payload", the_dict['cod']) + print("OWM API: observation data not available") + return None try: place = location.Location.from_dict(the_dict) except KeyError: diff --git a/pyowm/weatherapi25/stationhistory.py b/pyowm/weatherapi25/stationhistory.py index 64e2009d..b365deec 100644 --- a/pyowm/weatherapi25/stationhistory.py +++ b/pyowm/weatherapi25/stationhistory.py @@ -74,10 +74,9 @@ def from_dict(cls, d): # are found for a station and when the station does not exist! measurements = {} try: - if 'cod' in d: - if d['cod'] != "200": - raise exceptions.APIResponseError( - "OWM API: error - response payload: " + str(d), d['cod']) + if 'cod' in d and d['cod'] != "200": + raise exceptions.APIResponseError( + "OWM API: error - response payload: " + str(d), d['cod']) if str(d['cnt']) == "0": return None else: diff --git a/pyowm/weatherapi25/weather.py b/pyowm/weatherapi25/weather.py index d66ea17d..9aed38b9 100644 --- a/pyowm/weatherapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -206,8 +206,8 @@ def temperature(self, unit='kelvin'): """ # This is due to the fact that the OWM Weather API responses are mixing # absolute temperatures and temperature deltas together - to_be_converted = dict() - not_to_be_converted = dict() + to_be_converted = {} + not_to_be_converted = {} for label, temp in self.temp.items(): if temp is None or temp < 0: not_to_be_converted[label] = temp @@ -306,7 +306,7 @@ def from_dict(cls, the_dict): elif 'distance' in the_dict['visibility']: visibility_distance = the_dict['visibility']['distance'] elif 'last' in the_dict and 'visibility' in the_dict['last']: - if isinstance(the_dict['last']['visibility'], int) or isinstance(the_dict['last']['visibility'], float): + if isinstance(the_dict['last']['visibility'], (int, float)): visibility_distance = the_dict['last']['visibility'] elif 'distance' in the_dict['last']['visibility']: visibility_distance = the_dict['last']['visibility']['distance'] @@ -314,7 +314,7 @@ def from_dict(cls, the_dict): # -- clouds clouds = 0 if 'clouds' in the_dict: - if isinstance(the_dict['clouds'], int) or isinstance(the_dict['clouds'], float): + if isinstance(the_dict['clouds'], (int, float)): clouds = the_dict['clouds'] elif 'all' in the_dict['clouds']: clouds = the_dict['clouds']['all'] @@ -324,16 +324,16 @@ def from_dict(cls, the_dict): the_dict['rain'] = the_dict['precipitation'] # -- rain - rain = dict() + rain = {} if 'rain' in the_dict: - if isinstance(the_dict['rain'], int) or isinstance(the_dict['rain'], float): + if isinstance(the_dict['rain'], (int, float)): rain = {'all': the_dict['rain']} else: if the_dict['rain'] is not None: rain = the_dict['rain'].copy() # -- wind - wind = dict() + wind = {} if 'wind' in the_dict and the_dict['wind'] is not None: wind = the_dict['wind'].copy() elif 'last' in the_dict: @@ -364,9 +364,9 @@ def from_dict(cls, the_dict): humidity = 0 # -- snow - snow = dict() + snow = {} if 'snow' in the_dict: - if isinstance(the_dict['snow'], int) or isinstance(the_dict['snow'], float): + if isinstance(the_dict['snow'], (int, float)): snow = {'all': the_dict['snow']} else: if the_dict['snow'] is not None: @@ -389,9 +389,9 @@ def from_dict(cls, the_dict): pressure = {'press': atm_press, 'sea_level': sea_level_press} # -- temperature - temperature = dict() + temperature = {} if 'temp' in the_dict: - if isinstance(the_dict['temp'], int) or isinstance(the_dict['temp'], float): + if isinstance(the_dict['temp'], (int, float)): temperature = { 'temp': the_dict.get('temp', None) } @@ -413,7 +413,7 @@ def from_dict(cls, the_dict): # add feels_like to temperature if present if 'feels_like' in the_dict: feels_like = the_dict['feels_like'] - if isinstance(feels_like, int) or isinstance(feels_like, float): + if isinstance(feels_like, (int, float)): temperature['feels_like'] = the_dict.get('feels_like', None) elif isinstance(feels_like, dict): for label, temp in feels_like.items(): @@ -432,11 +432,7 @@ def from_dict(cls, the_dict): weather_icon_name = '' # -- timezone - if 'timezone' in the_dict: - utc_offset = the_dict['timezone'] - else: - utc_offset = None - + utc_offset = the_dict['timezone'] if 'timezone' in the_dict else None # -- UV index uvi = the_dict.get('uvi', None) diff --git a/pyowm/weatherapi25/weather_manager.py b/pyowm/weatherapi25/weather_manager.py index 3d19c6e9..03b04f0b 100644 --- a/pyowm/weatherapi25/weather_manager.py +++ b/pyowm/weatherapi25/weather_manager.py @@ -164,7 +164,7 @@ def weather_at_places(self, pattern, searchtype, limit=None): """ assert isinstance(pattern, str), "'pattern' must be a str" assert isinstance(searchtype, str), "'searchtype' must be a str" - if searchtype != "accurate" and searchtype != "like": + if searchtype not in ["accurate", "like"]: raise ValueError("'searchtype' value must be 'accurate' or 'like'") if limit is not None: assert isinstance(limit, int), "'limit' must be an int or None" @@ -524,9 +524,10 @@ def one_call(self, lat: Union[int, float], lon: Union[int, float], **kwargs) -> geo.assert_is_lat(lat) params = {'lon': lon, 'lat': lat} for key , value in kwargs.items(): - if key == 'exclude': params['exclude'] = value - if key == 'units': params['units'] = value - + if key == 'exclude': + params['exclude'] = value + elif key == 'units': + params['units'] = value _, json_data = self.http_client.get_json(ONE_CALL_URI, params=params) return one_call.OneCall.from_dict(json_data) diff --git a/pyowm/weatherapi25/weathercoderegistry.py b/pyowm/weatherapi25/weathercoderegistry.py index 78bc34ea..c3d615f9 100644 --- a/pyowm/weatherapi25/weathercoderegistry.py +++ b/pyowm/weatherapi25/weathercoderegistry.py @@ -87,7 +87,7 @@ def status_for(self, code): :type code: int :returns: the weather status str or ``None`` if the code is not mapped """ - is_in = lambda start, end, n: True if start <= n <= end else False + is_in = lambda start, end, n: start <= n <= end for status in self._code_ranges_dict: for _range in self._code_ranges_dict[status]: if is_in(_range['start'],_range['end'],code): diff --git a/scripts/fill_entity_template.py b/scripts/fill_entity_template.py index 38a2fe38..7c6460c1 100644 --- a/scripts/fill_entity_template.py +++ b/scripts/fill_entity_template.py @@ -11,10 +11,7 @@ try: param_2 = sys.argv[2] - if param_2 == '--iterable': - is_iterable = True - else: - is_iterable = False + is_iterable = param_2 == '--iterable' except: is_iterable = False diff --git a/scripts/generate_city_id_files.py b/scripts/generate_city_id_files.py index 59646d06..3fa2fd9a 100644 --- a/scripts/generate_city_id_files.py +++ b/scripts/generate_city_id_files.py @@ -27,7 +27,7 @@ def download_the_files(): def read_all_cities_into_dict(): print('Reading city data from files ...') - all_cities = dict() + all_cities = {} # All cities with gzip.open(city_list_gz, "rb", "utf-8") as i: @@ -76,21 +76,16 @@ def split_keyset(cities_dict): continue c = ord(name[0]) if c < 97: # not a letter - continue + pass elif c in range(97, 103): # from a to f ss[0].append(city_to_string(city_id, cities_dict[city_id])) - continue elif c in range(103, 109): # from g to l ss[1].append(city_to_string(city_id, cities_dict[city_id])) - continue elif c in range(109, 115): # from m to r ss[2].append(city_to_string(city_id, cities_dict[city_id])) - continue elif c in range (115, 123): # from s to z ss[3].append(city_to_string(city_id, cities_dict[city_id])) - continue - else: - continue # not a letter + continue print('... done') return ss From b13a354668cf651cc5705dac0f03487c29bd8a81 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 20 Dec 2020 19:53:51 +0100 Subject: [PATCH 20/36] Fixes #358 --- sphinx/v3/code-recipes.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index bfb58ab9..ff82229a 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -87,6 +87,10 @@ owm = OWM('your-api-key', config_dict) ``` ### Language setting +The OWM API can be asked to return localized *detailed statuses* for weather data +In PyOWM this means that you can specify a language and you'll retrieve `Weather` objects having the `detailed_status` +field localized in that language. Localization is not provided for `status` field instead, so pay attention to that. + The list of supported languages is given by: ```python from pyowm.owm import OWM @@ -101,8 +105,11 @@ English is the default language on the OWM API - but you can change it: from pyowm.owm import OWM from pyowm.utils.config import get_default_config config_dict = get_default_config() -config_dict['language'] = 'pt' # your language here, eg. Portuguese +config_dict['language'] = 'fr' # your language here, eg. French owm = OWM('your-api-key', config_dict) +mgr = owm.weather_manager() +observation = mgr.weather_at_place('Paris, FR') +observation.weather.detailed_status # Nuageux ``` ### Get PyOWM configuration From 427fa955dcf4472e1fd5ba111598b85f4d338f36 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 20 Dec 2020 20:15:04 +0100 Subject: [PATCH 21/36] Fixes #360 --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3671e788..999518b4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -19,6 +19,7 @@ Code * [lardconcepts](https://github.com/lardconcepts) * [liato](https://github.com/liato) * [LukasBoersma](https://github.com/LukasBoersma) + * [MatthiasLohr](https://github.com/MatthiasLohr) * [Misiu](https://github.com/Misiu) * [Noid](https://github.com/n0id) * [titilambert](https://github.com/titilambert) From 3566c1f16cbc66b8b27fedcb8b9cf33a12ebdab4 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 20 Dec 2020 20:16:28 +0100 Subject: [PATCH 22/36] Fixes #360 (forgot..) --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3c0576a8..bd22e8b0 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,9 @@ from pyowm.__version__ import __author__, __author_email__, __description__, __license__, __title__,\ __url__, __version__ +with open('README.md', 'r') as readme: + long_description = readme.read() + setup( name=__title__, version=__version__, @@ -12,8 +15,8 @@ author_email=__author_email__, url=__url__, packages=find_packages(exclude=['tests']), - long_description="""PyOWM is a client Python wrapper library for OpenWeatherMap web APIs. It allows quick and easy - consumption of OWM data from Python applications via a simple object model and in a human-friendly fashion.""", + long_description=long_description, + long_description_content_type='text/markdown', include_package_data=True, install_requires=[ 'requests>=2.20.0,<3', From 0eb016b29bda31f04284db3817d507de06f4a5f6 Mon Sep 17 00:00:00 2001 From: csparpa Date: Tue, 29 Dec 2020 14:20:41 +0100 Subject: [PATCH 23/36] Fix typo (as per #363) --- sphinx/v3/code-recipes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index ff82229a..dd0c7838 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -410,7 +410,7 @@ Also rain amount is a dict, with keys: `1h` an `3h`, containing the mms of rain from pyowm.owm import OWM owm = OWM('your-api-key') mgr = owm.weather_manager() -rain_dict = mgr.weather_at_place('Berlin,DE').observation.rain +rain_dict = mgr.weather_at_place('Berlin,DE').weather.rain rain_dict['1h'] rain_dict['3h'] ``` From 1e3ba96a6cd241e852fce84f29ed451772b9a029 Mon Sep 17 00:00:00 2001 From: csparpa Date: Mon, 25 Jan 2021 19:50:58 +0100 Subject: [PATCH 24/36] New Air Pollution retrieval API supported - fixes #362 --- .../airpollutionapi30/airpollution_client.py | 14 +- .../airpollutionapi30/airpollution_manager.py | 33 ++++- pyowm/airpollutionapi30/airstatus.py | 131 ++++++++++++++++++ pyowm/airpollutionapi30/uris.py | 6 + sphinx/v3/code-recipes.md | 32 +++++ .../test_integration_pollutionapi30.py | 53 ++----- .../test_airpollution_manager.py | 31 ++++- .../unit/airpollutionapi30/test_airstatus.py | 96 +++++++++++++ 8 files changed, 346 insertions(+), 50 deletions(-) create mode 100644 pyowm/airpollutionapi30/airstatus.py create mode 100644 tests/unit/airpollutionapi30/test_airstatus.py diff --git a/pyowm/airpollutionapi30/airpollution_client.py b/pyowm/airpollutionapi30/airpollution_client.py index 3238fe2a..07a54a63 100644 --- a/pyowm/airpollutionapi30/airpollution_client.py +++ b/pyowm/airpollutionapi30/airpollution_client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL +from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL, AIR_POLLUTION_URL from pyowm.utils import formatting @@ -152,6 +152,18 @@ def get_so2(self, params_dict): _, json_data = self._client.get_json(uri) return json_data + def get_air_pollution(self, params_dict): + """ + Invokes the new AirPollution API endpoint + + :param params_dict: dict of parameters + :returns: a string containing raw JSON data + :raises: *ValueError*, *APIRequestError* + + """ + _, json_data = self._client.get_json(AIR_POLLUTION_URL, params=params_dict) + return json_data + def __repr__(self): return "<%s.%s - httpclient=%s>" % \ (__name__, self.__class__.__name__, str(self._client)) diff --git a/pyowm/airpollutionapi30/airpollution_manager.py b/pyowm/airpollutionapi30/airpollution_manager.py index 1a9076b7..0151dffc 100644 --- a/pyowm/airpollutionapi30/airpollution_manager.py +++ b/pyowm/airpollutionapi30/airpollution_manager.py @@ -1,11 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pyowm.airpollutionapi30 import airpollution_client, coindex, no2index, ozone, so2index -from pyowm.airpollutionapi30.uris import ROOT_POLLUTION_API_URL +from pyowm.airpollutionapi30 import airpollution_client, coindex, no2index, ozone, so2index, airstatus +from pyowm.airpollutionapi30.uris import ROOT_POLLUTION_API_URL, NEW_ROOT_POLLUTION_API_URL from pyowm.commons.http_client import HttpClient from pyowm.constants import AIRPOLLUTION_API_VERSION -from pyowm.utils import geo +from pyowm.utils import geo, decorators class AirPollutionManager: @@ -29,10 +29,14 @@ def __init__(self, API_key, config): self.ap_client = airpollution_client.AirPollutionHttpClient( API_key, HttpClient(API_key, config, ROOT_POLLUTION_API_URL)) + self.new_ap_client = airpollution_client.AirPollutionHttpClient( + API_key, + HttpClient(API_key, config, NEW_ROOT_POLLUTION_API_URL)) def airpollution_api_version(self): return AIRPOLLUTION_API_VERSION + @decorators.deprecated('removed', '4', 'coindex_around_coords') def coindex_around_coords(self, lat, lon, start=None, interval=None): """ 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): coi.interval = interval return coi + @decorators.deprecated('removed', '4', 'ozone_around_coords') def ozone_around_coords(self, lat, lon, start=None, interval=None): """ 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): oz.interval = interval return oz + @decorators.deprecated('removed', '4', 'no2index_around_coords') def no2index_around_coords(self, lat, lon, start=None, interval=None): """ 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): no2.interval = interval return no2 + @decorators.deprecated('removed', '4', 'so2index_around_coords') def so2index_around_coords(self, lat, lon, start=None, interval=None): """ 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): so2.interval = interval return so2 + def air_quality_at_coords(self, lat, lon): + """ + Queries the OWM AirPollution API for all available air quality indicators around the specified coordinates. + + :param lat: the location's latitude, must be between -90.0 and 90.0 + :type lat: int/float + :param lon: the location's longitude, must be between -180.0 and 180.0 + :type lon: int/float + :return: a *AirStatus* instance or ``None`` if data is not available + :raises: *ParseResponseException* when OWM AirPollution API responses' data + cannot be parsed, *APICallException* when OWM AirPollution API can not be + reached, *ValueError* for wrong input values + """ + geo.assert_is_lon(lon) + geo.assert_is_lat(lat) + params = {'lon': lon, 'lat': lat} + json_data = self.new_ap_client.get_air_pollution(params) + air_status = airstatus.AirStatus.from_dict(json_data) + return air_status + def __repr__(self): return '<%s.%s>' % (__name__, self.__class__.__name__) diff --git a/pyowm/airpollutionapi30/airstatus.py b/pyowm/airpollutionapi30/airstatus.py new file mode 100644 index 00000000..e96a13f0 --- /dev/null +++ b/pyowm/airpollutionapi30/airstatus.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pyowm.commons import exceptions +from pyowm.utils import formatting, timestamps +from pyowm.weatherapi25 import location + + +class AirStatus: + """ + A class representing a dataset about air quality + + :param reference_time: GMT UNIXtime telling when the data has been measured + :type reference_time: int + :param location: the *Location* relative to this measurement + :type location: *Location* + :param interval: the time granularity of the CO observation + :type interval: str + :param air_quality_data: the dataset + :type air_quality_data: dict + :param reception_time: GMT UNIXtime telling when the CO observation has + been received from the OWM Weather API + :type reception_time: int + :returns: an *COIndex* instance + :raises: *ValueError* when negative values are provided as reception time, + CO samples are not provided in a list + + """ + + def __init__(self, reference_time, location, air_quality_data, reception_time): + if reference_time < 0: + raise ValueError("'reference_time' must be greater than 0") + self.ref_time = reference_time + self.location = location + if not isinstance(air_quality_data, dict): + raise ValueError("'air_quality_data' must be a list") + self.air_quality_data = air_quality_data + for key, val in air_quality_data.items(): + setattr(self, key, val) + if reception_time < 0: + raise ValueError("'reception_time' must be greater than 0") + self.rec_time = reception_time + + def reference_time(self, timeformat='unix'): + """ + Returns the GMT time telling when the air quality data have been measured + + :param timeformat: the format for the time value. May be: + '*unix*' (default) for UNIX time + '*iso*' for ISO8601-formatted string in the format ``YYYY-MM-DD HH:MM:SS+00:00`` + '*date* for ``datetime.datetime`` object instance + :type timeformat: str + :returns: an int or a str + :raises: ValueError when negative values are provided + + """ + return formatting.timeformat(self.ref_time, timeformat) + + def reception_time(self, timeformat='unix'): + """ + Returns the GMT time telling when the air quality data has been received + from the OWM Weather API + + :param timeformat: the format for the time value. May be: + '*unix*' (default) for UNIX time + '*iso*' for ISO8601-formatted string in the format ``YYYY-MM-DD HH:MM:SS+00:00`` + '*date* for ``datetime.datetime`` object instance + :type timeformat: str + :returns: an int or a str + :raises: ValueError when negative values are provided + + """ + return formatting.timeformat(self.rec_time, timeformat) + + + @classmethod + def from_dict(cls, the_dict): + """ + Parses a *AirStatus* instance out of a data dictionary. + + :param the_dict: the input dictionary + :type the_dict: `dict` + :returns: a *AirStatus* instance or ``None`` if no data is available + :raises: *ParseAPIResponseError* if it is impossible to find or parse the data needed to build the result + + """ + if the_dict is None: + raise exceptions.ParseAPIResponseError('Data is None') + try: + + item = the_dict['list'][0] + + # -- reference time (strip away Z and T on ISO8601 format) + reference_time = item['dt'] + + # -- reception time (now) + reception_time = timestamps.now('unix') + + # -- location + lon = float(the_dict['coord']['lat']) + lat = float(the_dict['coord']['lon']) + place = location.Location(None, lon, lat, None) + + # -- air quality data + data = item['components'] + data['aqi'] = item['main']['aqi'] + + except KeyError: + raise exceptions.ParseAPIResponseError( + ''.join([__name__, ': impossible to parse AirStatus'])) + + return AirStatus(reference_time, place, data, reception_time) + + def to_dict(self): + """Dumps object to a dictionary + + :returns: a `dict` + + """ + return {"reference_time": self.ref_time, + "location": self.location.to_dict(), + "air_quality_data": self.air_quality_data, + "reception_time": self.rec_time} + + def __repr__(self): + return "<%s.%s - reference time=%s, reception time=%s, location=%s" % ( + __name__, + self.__class__.__name__, + self.reference_time('iso'), + self.reception_time('iso'), + str(self.location)) diff --git a/pyowm/airpollutionapi30/uris.py b/pyowm/airpollutionapi30/uris.py index 2591623d..cfbd77d9 100644 --- a/pyowm/airpollutionapi30/uris.py +++ b/pyowm/airpollutionapi30/uris.py @@ -1,8 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# deprecated API endpoints ROOT_POLLUTION_API_URL = 'openweathermap.org/pollution/v1' CO_INDEX_URL = 'co' OZONE_URL = 'o3' NO2_INDEX_URL = 'no2' SO2_INDEX_URL = 'so2' + + +# current API endpoint +NEW_ROOT_POLLUTION_API_URL = 'openweathermap.org/data/2.5' +AIR_POLLUTION_URL = 'air_pollution' diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index dd0c7838..d46736d8 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -8,6 +8,7 @@ Table of contents: * [Identifying cities and places via city IDs](#identifying_places) * [OneCall data](#onecall) * [Weather data](#weather_data) + * [Air pollution data](#airpollution_data) * [Weather forecasts](#weather_forecasts) * [Meteostation historic measurements](#station_measurements) @@ -754,6 +755,37 @@ TBD TBD +
+ +## Air pollution data + +Instead of getting a `weather_manager`, get from the main OWM object a `airpollution_manager` and use it + +### Getting air polluting concentrations and Air Quality Index on geographic coords +Air polluting agents concentration can be queried in one shot: + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.airpollution_manager() + +air_status = mgr.air_quality_at_coords(51.507351, -0.127758) # London, GB + +# you can then get values for all of these air pollutants +air_status.co +air_status.no +air_status.no2 +air_status.o3 +air_status.so2 +air_status.pm2_5 +air_status.pm10 +air_status.nh3 + +# and for air quality index +air_status.aqi +``` + +
diff --git a/tests/integration/pollutionapi30/test_integration_pollutionapi30.py b/tests/integration/pollutionapi30/test_integration_pollutionapi30.py index e1163501..a789c589 100644 --- a/tests/integration/pollutionapi30/test_integration_pollutionapi30.py +++ b/tests/integration/pollutionapi30/test_integration_pollutionapi30.py @@ -10,53 +10,16 @@ class IntegrationTestsPollutionAPI30(unittest.TestCase): __owm = owm.OWM(os.getenv('OWM_API_KEY', None)).airpollution_manager() - def test_coindex_around_coords(self): + def test_air_quality_at_coords(self): """ - Test feature: get CO index around geo-coordinates. + Test feature: get all air quality data around geo-coordinates. """ - u = self.__owm.coindex_around_coords(45, 9) - self.assertIsNotNone(u) - self.assertIsNotNone(u.co_samples) - self.assertIsNotNone(u.reception_time()) - self.assertIsNotNone(u.reference_time()) - self.assertIsNone(u.interval) - self.assertIsNotNone(u.location) - - def test_ozone_around_coords(self): - """ - Test feature: get ozone around geo-coordinates. - """ - u = self.__owm.ozone_around_coords(0.0, 10.0, start='2016-12-31 12:55:55+00:00') - self.assertIsNotNone(u) - self.assertIsNotNone(u.du_value) - self.assertIsNotNone(u.reception_time()) - self.assertIsNotNone(u.reference_time()) - self.assertIsNone(u.interval) - self.assertIsNotNone(u.location) - - def test_no2index_around_coords(self): - """ - Test feature: get NO2 index around geo-coordinates. - """ - u = self.__owm.no2index_around_coords(0.0, 10.0, start='2016-12-31 12:55:55+00:00') - self.assertIsNotNone(u) - self.assertIsNotNone(u.no2_samples) - self.assertIsNotNone(u.reception_time()) - self.assertIsNotNone(u.reference_time()) - self.assertIsNone(u.interval) - self.assertIsNotNone(u.location) - - def test_so2index_around_coords(self): - """ - Test feature: get SO2 index around geo-coordinates. - """ - u = self.__owm.so2index_around_coords(0.0, 10.0, start='2016-12-31 12:55:55+00:00') - self.assertIsNotNone(u) - self.assertIsNotNone(u.so2_samples) - self.assertIsNotNone(u.reception_time()) - self.assertIsNotNone(u.reference_time()) - self.assertIsNone(u.interval) - self.assertIsNotNone(u.location) + airstatus = self.__owm.air_quality_at_coords(45, 9) + self.assertIsNotNone(airstatus) + self.assertIsNotNone(airstatus.air_quality_data) + self.assertIsNotNone(airstatus.reception_time()) + self.assertIsNotNone(airstatus.reference_time()) + self.assertIsNotNone(airstatus.location) if __name__ == "__main__": diff --git a/tests/unit/airpollutionapi30/test_airpollution_manager.py b/tests/unit/airpollutionapi30/test_airpollution_manager.py index ba4c6e0c..01f71194 100644 --- a/tests/unit/airpollutionapi30/test_airpollution_manager.py +++ b/tests/unit/airpollutionapi30/test_airpollution_manager.py @@ -3,13 +3,14 @@ import json import unittest -from pyowm.airpollutionapi30 import airpollution_client, airpollution_manager, coindex, so2index, ozone, no2index +from pyowm.airpollutionapi30 import airpollution_client, airpollution_manager, coindex, so2index, ozone, no2index, airstatus from pyowm.config import DEFAULT_CONFIG from pyowm.constants import AIRPOLLUTION_API_VERSION from tests.unit.airpollutionapi30.test_ozone import OZONE_JSON from tests.unit.airpollutionapi30.test_coindex import COINDEX_JSON from tests.unit.airpollutionapi30.test_no2index import NO2INDEX_JSON from tests.unit.airpollutionapi30.test_so2index import SO2INDEX_JSON +from tests.unit.airpollutionapi30.test_airstatus import AIRSTATUS_JSON class TestAirPollutionManager(unittest.TestCase): @@ -25,6 +26,9 @@ def mock_get_o3_returning_ozone_around_coords(self, params_dict): def mock_get_no2_returning_no2index_around_coords(self, params_dict): return json.loads(NO2INDEX_JSON) + def mock_get_air_pollution(self, params_dict): + return json.loads(AIRSTATUS_JSON) + def mock_get_so2_returning_so2index_around_coords(self, params_dict): return json.loads(SO2INDEX_JSON) @@ -170,5 +174,30 @@ def test_so2index_around_coords_fails_with_wrong_parameters(self): self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.so2index_around_coords, \ self.__test_instance, 200, 2.5) + def test_air_quality_at_coords(self): + ref_to_original = airpollution_client.AirPollutionHttpClient.get_air_pollution + airpollution_client.AirPollutionHttpClient.get_air_pollution = \ + self.mock_get_air_pollution + result = self.__test_instance.air_quality_at_coords(45, 9) + airpollution_client.AirPollutionHttpClient.get_air_pollution = ref_to_original + self.assertTrue(isinstance(result, airstatus.AirStatus)) + self.assertIsNotNone(result.reference_time) + self.assertIsNotNone(result.reception_time()) + loc = result.location + self.assertIsNotNone(loc) + self.assertIsNotNone(loc.lat) + self.assertIsNotNone(loc.lon) + self.assertIsNotNone(result.air_quality_data) + + def test_air_quality_at_coords_fails_with_wrong_parameters(self): + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_at_coords, \ + self.__test_instance, 43.7, -200.0) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_at_coords, \ + self.__test_instance, 43.7, 200.0) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_at_coords, \ + self.__test_instance, -200, 2.5) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_at_coords, \ + self.__test_instance, 200, 2.5) + def test_repr(self): print(self.__test_instance) diff --git a/tests/unit/airpollutionapi30/test_airstatus.py b/tests/unit/airpollutionapi30/test_airstatus.py new file mode 100644 index 00000000..66d8b6b1 --- /dev/null +++ b/tests/unit/airpollutionapi30/test_airstatus.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import unittest +import json +from datetime import datetime +import pyowm.commons.exceptions +from pyowm.weatherapi25.location import Location +from pyowm.airpollutionapi30.airstatus import AirStatus + + +AIRSTATUS_JSON = '{"coord":{"lon":-0.1278,"lat":51.5074},"list":[{"main":{"aqi":1},"components":{"co":250.34,"no":0.19,"no2":35.99,"o3":30.76,"so2":8.11,"pm2_5":3.15,"pm10":3.81,"nh3":0.74},"dt":1611597600}]}' +AIRSTATUS_MALFORMED_JSON = '{"time":"2016-10-01T13:07:01Z","xyz":[]}' +AIRSTATUS_JSON_DUMP = '{"reference_time": 1234567, "location": {"name": "test", "coordinates": {"lon": 12.3, "lat": 43.7}, "ID": 987, "country": "UK"}, "air_quality_data": {"aqi": 1, "co": 250.34, "no": 0.19, "no2": 35.99, "o3": 30.76, "so2": 8.11, "pm2_5": 3.15, "pm10": 3.81, "nh3": 0.74}, "reception_time": 1475283600}' + + +class TestAirStatus(unittest.TestCase): + + __test_reception_time = 1475283600 + __test_iso_reception_time = "2016-10-01 01:00:00+00:00" + __test_date_reception_time = datetime.fromisoformat(__test_iso_reception_time) + + __test_reference_time = 1234567 + __test_iso_reference_time = "1970-01-15 06:56:07+00:00" + __test_date_reference_time = datetime.fromisoformat(__test_iso_reference_time) + __test_location = Location('test', 12.3, 43.7, 987, 'UK') + __test_air_quality_data = {"aqi": 1, "co": 250.34, "no": 0.19, "no2": 35.99, "o3": 30.76, "so2": 8.11, "pm2_5": 3.15, "pm10": 3.81, "nh3": 0.74} + __test_interval = 'day' + __test_instance = AirStatus( + __test_reference_time, __test_location, __test_air_quality_data, __test_reception_time) + + def test_init_fails_when_reference_time_is_negative(self): + self.assertRaises(ValueError, AirStatus, -1234567, + self.__test_location, + self.__test_air_quality_data, + self.__test_reception_time) + + def test_init_fails_when_reception_time_is_negative(self): + self.assertRaises(ValueError, AirStatus, + self.__test_reference_time, + self.__test_location, + self.__test_air_quality_data, + -1234567) + + def test_init_fails_when_air_quality_data_is_not_a_dict(self): + self.assertRaises(ValueError, AirStatus, self.__test_reference_time, + self.__test_location, 'test', + self.__test_reception_time) + + def test_returning_different_formats_for_reference_time(self): + self.assertEqual(self.__test_instance.reference_time(timeformat='iso'), \ + self.__test_iso_reference_time) + self.assertEqual(self.__test_instance.reference_time(timeformat='unix'), \ + self.__test_reference_time) + self.assertEqual(self.__test_instance.reference_time(timeformat='date'), \ + self.__test_date_reference_time) + + def test_returning_different_formats_for_reception_time(self): + self.assertEqual(self.__test_instance.reception_time(timeformat='iso'), \ + self.__test_iso_reception_time) + self.assertEqual(self.__test_instance.reception_time(timeformat='unix'), \ + self.__test_reception_time) + self.assertEqual(self.__test_instance.reception_time(timeformat='date'), \ + self.__test_date_reception_time) + + def test_from_dict(self): + d = json.loads(AIRSTATUS_JSON) + result = AirStatus.from_dict(d) + self.assertIsNotNone(result) + self.assertIsNotNone(result.reference_time()) + self.assertIsNotNone(result.reference_time()) + loc = result.location + self.assertIsNotNone(loc) + self.assertIsNone(loc.name) + self.assertIsNone(loc.id) + self.assertIsNotNone(loc.lon) + self.assertIsNotNone(loc.lat) + for key in self.__test_air_quality_data: + getattr(result, key) + + def test_from_dict_fails_when_JSON_data_is_None(self): + self.assertRaises(pyowm.commons.exceptions.ParseAPIResponseError, AirStatus.from_dict, None) + + def test_from_dict_fails_with_malformed_JSON_data(self): + self.assertRaises(pyowm.commons.exceptions.ParseAPIResponseError, AirStatus.from_dict, json.loads(AIRSTATUS_MALFORMED_JSON)) + + def test_to_dict(self): + expected = json.loads(AIRSTATUS_JSON_DUMP) + result = self.__test_instance.to_dict() + + ordered_str_expected = sorted(str(expected)) + ordered_str_result = sorted(str(result)) + self.assertEqual(ordered_str_expected, ordered_str_result) + + def test_repr(self): + print(self.__test_instance) From 0658555b42c660d37864f608a7ab79adb5da4199 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 31 Jan 2021 18:16:25 +0100 Subject: [PATCH 25/36] Applying #368 to the right branch --- sphinx/v3/code-recipes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index d46736d8..ab5d6f2e 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -216,7 +216,7 @@ list_of_geopoints = reg.geopoints_for('rome') With the OneCall Api you can get the current weather, hourly forecast for the next 48 hours and the daily forecast for the next seven days in one call. -One Call objects can be thought of as datasets that "photograhp" of observed and forecasted weather data for a location: such photos are given for a specific timestamp. +One Call objects can be thought of as datasets that "photograph" of observed and forecasted weather data for a location: such photos are given for a specific timestamp. It is possible to get: - current OneCall data: the "photo" given for today) From 128b09c9aed76e080286d3d1a7989bf0dc21a39f Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 31 Jan 2021 18:17:42 +0100 Subject: [PATCH 26/36] credits to #368 author --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b908466..91c0f8c0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -33,6 +33,7 @@ Docs * [EJEP](https://github.com/EJEP) * [Franzqat](https://github.com/franzqat) * [Harmon758](https://github.com/Harmon758) + * [joe-meyer](https://github.com/joe-meyer) Testing ------- From 715a29c9d7b802cb6008b69942006f6c3b82eb06 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 31 Jan 2021 18:20:45 +0100 Subject: [PATCH 27/36] Applying #367 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bd22e8b0..591ae528 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ author=__author__, author_email=__author_email__, url=__url__, - packages=find_packages(exclude=['tests']), + packages=find_packages(exclude=['tests','tests.*']), long_description=long_description, long_description_content_type='text/markdown', include_package_data=True, From 6731479d40c5e64ad9ee4c3bc0e06ea4419fa679 Mon Sep 17 00:00:00 2001 From: csparpa Date: Sun, 31 Jan 2021 22:17:48 +0100 Subject: [PATCH 28/36] Implemented Geocoding API support - fixes #364 --- README.md | 1 + pyowm/commons/uris.py | 7 ++ pyowm/constants.py | 1 + pyowm/geocodingapi10/__init__.py | 0 pyowm/geocodingapi10/geocoding_manager.py | 85 +++++++++++++ pyowm/owm.py | 9 ++ pyowm/weatherapi25/location.py | 3 + sphinx/index.rst | 10 ++ sphinx/pyowm.geocodingapi10.rst | 13 ++ sphinx/pyowm.rst | 1 + sphinx/v3/code-recipes.md | 81 +++++++++++-- sphinx/v3/geocoding-api-usage-examples.md | 8 ++ tests/integration/geocodingapi10/__init__.py | 0 .../test_integration_geocodingapi10.py | 31 +++++ tests/unit/geocodingapi10/__init__.py | 0 .../unit/geocodingapi10/geocoding_manager.py | 112 ++++++++++++++++++ tests/unit/test_owm.py | 6 + 17 files changed, 360 insertions(+), 8 deletions(-) create mode 100644 pyowm/commons/uris.py create mode 100644 pyowm/geocodingapi10/__init__.py create mode 100644 pyowm/geocodingapi10/geocoding_manager.py create mode 100644 sphinx/pyowm.geocodingapi10.rst create mode 100644 sphinx/v3/geocoding-api-usage-examples.md create mode 100644 tests/integration/geocodingapi10/__init__.py create mode 100644 tests/integration/geocodingapi10/test_integration_geocodingapi10.py create mode 100644 tests/unit/geocodingapi10/__init__.py create mode 100644 tests/unit/geocodingapi10/geocoding_manager.py diff --git a/README.md b/README.md index 43b40645..43cd262b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ With PyOWM you can integrate into your code any of the following OpenWeatherMap - **Stations API v3.0**, allowing to create and manage meteostation and publish local weather measurements - **Weather Alerts API v3.0**, allowing to set triggers on weather conditions and areas and poll for spawned alerts - **Image tiles** for several map layers provided by OWM + - **Geocoding API v1.0** allowing to perform direct/reverse geocoding ## Get started diff --git a/pyowm/commons/uris.py b/pyowm/commons/uris.py new file mode 100644 index 00000000..53fd9db4 --- /dev/null +++ b/pyowm/commons/uris.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +ROOT_GEOCODING_API_URL = 'openweathermap.org/geo/1.0' +DIRECT_GEOCODING_URI = 'direct' +REVERSE_GEOCODING_URI = 'reverse' + diff --git a/pyowm/constants.py b/pyowm/constants.py index 22753ffb..75444de7 100644 --- a/pyowm/constants.py +++ b/pyowm/constants.py @@ -5,6 +5,7 @@ AGRO_API_VERSION = (1, 0, 0) AIRPOLLUTION_API_VERSION = (3, 0, 0) ALERT_API_VERSION = (3, 0, 0) +GEOCODING_API_VERSION = (1, 0, 0) STATIONS_API_VERSION = (3, 0, 0) UVINDEX_API_VERSION = (3, 0, 0) WEATHER_API_VERSION = (2, 5, 0) diff --git a/pyowm/geocodingapi10/__init__.py b/pyowm/geocodingapi10/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyowm/geocodingapi10/geocoding_manager.py b/pyowm/geocodingapi10/geocoding_manager.py new file mode 100644 index 00000000..5e02efd8 --- /dev/null +++ b/pyowm/geocodingapi10/geocoding_manager.py @@ -0,0 +1,85 @@ +from pyowm.commons.http_client import HttpClient +from pyowm.commons.uris import ROOT_GEOCODING_API_URL, DIRECT_GEOCODING_URI, REVERSE_GEOCODING_URI +from pyowm.constants import GEOCODING_API_VERSION +from pyowm.utils import geo +from pyowm.weatherapi25.location import Location + + +class GeocodingManager: + + """ + A manager objects that provides a full interface to OWM Geocoding API. + + :param API_key: the OWM API key + :type API_key: str + :param config: the configuration dictionary + :type config: dict + :returns: an *GeocodingManager* instance + :raises: *AssertionError* when no API Key is provided + + """ + + def __init__(self, API_key, config): + assert API_key is not None, 'You must provide a valid API Key' + self.API_key = API_key + assert isinstance(config, dict) + self.http_client = HttpClient(API_key, config, ROOT_GEOCODING_API_URL) + + def geocoding_api_version(self): + return GEOCODING_API_VERSION + + def geocode(self, toponym, country=None, state_code=None, limit=None): + """ + Invokes the direct geocoding API endpoint + + :param toponym: the name of the location + :type toponym: `str` + :param country: the 2-chars ISO symbol of the country + :type country: `str` or `None` + :param state_code: the 2-chars ISO symbol of state (only useful in case the country is US) + :type state_code: `str` or `None` + :param limit: the max number of results to be returned in case of multiple matchings (no limits by default) + :type limit: `int` or `None` + :returns: a list of *Location* instances + :raises: *AssertionError*, *ValueError*, *APIRequestError* + + """ + assert toponym, 'Toponym must be specified' + if country is not None and len(country) != 2: + raise ValueError("Country must be a 2-char string") + if state_code is not None and len(state_code) != 2: + raise ValueError("State Code must be a 2-char string") + if limit is not None: + assert isinstance(limit, int) + assert limit > 0 + + query = toponym + if state_code is not None: + query += ',' + state_code + if country is not None: + query += ',' + country + + params = {'q': query} + + if limit is not None: + params['limit'] = limit + + _, json_data = self.http_client.get_json(DIRECT_GEOCODING_URI, params=params) + return [Location.from_dict(item) for item in json_data] + + def reverse_geocode(self, lat, lon, limit=None): + geo.assert_is_lon(lon) + geo.assert_is_lat(lat) + if limit is not None: + assert isinstance(limit, int) + assert limit > 0 + + params = {'lat': lat, 'lon': lon} + if limit is not None: + params['limit'] = limit + + _, json_data = self.http_client.get_json(REVERSE_GEOCODING_URI, params=params) + return [Location.from_dict(item) for item in json_data] + + def __repr__(self): + return '<%s.%s>' % (__name__, self.__class__.__name__) \ No newline at end of file diff --git a/pyowm/owm.py b/pyowm/owm.py index 869bf664..a5e90b3a 100644 --- a/pyowm/owm.py +++ b/pyowm/owm.py @@ -5,6 +5,7 @@ from pyowm.agroapi10 import agro_manager from pyowm.airpollutionapi30 import airpollution_manager from pyowm.alertapi30 import alert_manager +from pyowm.geocodingapi10 import geocoding_manager from pyowm.stationsapi30 import stations_manager from pyowm.tiles import tile_manager from pyowm.utils import strings @@ -125,6 +126,14 @@ def weather_manager(self): """ return weather_manager.WeatherManager(self.api_key, self.config) + def geocoding_manager(self): + """ + Gives a `pyowm.geocoding10.geocoding_manager.GeocodingManager` instance that can be used to perform direct + and reverse geocoding + :return: a `pyowm.geocoding10.geocoding_manager.GeocodingManager` instance + """ + return geocoding_manager.GeocodingManager(self.api_key, self.config) + def __repr__(self): return "<%s.%s - API key=%s, subscription type=%s, PyOWM version=%s>" % \ (__name__, diff --git a/pyowm/weatherapi25/location.py b/pyowm/weatherapi25/location.py index e2570f65..3d79975f 100644 --- a/pyowm/weatherapi25/location.py +++ b/pyowm/weatherapi25/location.py @@ -83,6 +83,9 @@ def from_dict(cls, the_dict): else: lon = 0.0 lat = data['station']['coord'].get('lat', 0.0) + elif 'lat' in the_dict and 'lon' in the_dict: + lat = the_dict['lat'] + lon = the_dict['lon'] else: raise KeyError("Impossible to read geographical coordinates from JSON") if 'country' in data: diff --git a/sphinx/index.rst b/sphinx/index.rst index bae9781f..52897038 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -37,6 +37,7 @@ With PyOWM you can interact programmatically with the following OpenWeatherMap w - **UV Index API v3.0**, offering data about Ultraviolet exposition - **Stations API v3.0**, allowing to create and manage meteostation and publish local weather measurements - **Weather Alerts API v3.0**, allowing to set triggers on weather conditions and areas and poll for spawned alerts + - **Geocoding API v1.0** allowing to perform direct/reverse geocoding And you can also get **image tiles** for several map layers provided by OWM @@ -183,6 +184,15 @@ Alerts API examples v3/alerts-api-usage-examples +Geocoding API examples +^^^^^^^^^^^^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + v3/geocoding-api-usage-examples + + Map tiles client examples ^^^^^^^^^^^^^^^^^^^^^^^^^ .. toctree:: diff --git a/sphinx/pyowm.geocodingapi10.rst b/sphinx/pyowm.geocodingapi10.rst new file mode 100644 index 00000000..b3810ae2 --- /dev/null +++ b/sphinx/pyowm.geocodingapi10.rst @@ -0,0 +1,13 @@ +pyowm.geocodingapi10 package +============================ + +Submodules +---------- + +pyowm.geocodingapi10.geocoding_manager module +--------------------------------------------- + +.. automodule:: pyowm.geocodingapi10.geocoding_manager + :members: + :undoc-members: + :show-inheritance: diff --git a/sphinx/pyowm.rst b/sphinx/pyowm.rst index 69857d0f..4ef217c3 100644 --- a/sphinx/pyowm.rst +++ b/sphinx/pyowm.rst @@ -10,6 +10,7 @@ Subpackages pyowm.alertapi30 pyowm.commons pyowm.airpollutionapi30 + pyowm.geocodingapi10 pyowm.stationsapi30 pyowm.tiles pyowm.utils diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index ab5d6f2e..8575ae7c 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -130,9 +130,16 @@ version_tuple = (major, minor, patch) = owm.version
-## Identifying cities and places via city IDs +## Identifying cities and places -### Obtain the city ID registry +You can easily get the City ID of a known toponym, as well as its geographic coordinates +Also you can leverage direct/reverse geocoding + +### City IDs + +The following calls will not result in any OWM API call in the background, so they will only happen locally to your machine. + +#### Obtain the city ID registry Use the city ID registry to lookup the ID of a city given its name ```python from pyowm.owm import OWM @@ -140,7 +147,7 @@ owm = OWM('your-api-key') city_id_registry = owm.city_id_registry() ``` -### Get the ID of a city given its name +#### Get the ID of a city given its name Don't forget that there is a high probabilty that your city is not unique in the world, and multiple cities with the same name exist in other countries Therefore specify toponyms and country 2-letter names separated by comma. Eg: if you search for the British `London` you'll likely multiple results: you then should also specify the country (`GB`) to narrow the search only to Great Britain. @@ -165,9 +172,8 @@ list_of_tuples = london = reg.ids_for('LoNdoN', country='GB') # and would get the very same results as above. -### Get the IDs of cities whose name contain a specific string - -In order yo find all cities with names having your string as a substring you need to use the optional parameter `matching='like'` +#### Get the IDs of cities whose name contain a specific string +In order to find all cities with names having your string as a substring you need to use the optional parameter `matching='like'` In example, let's find IDs for all British cities having the string `london` in their names: @@ -182,7 +188,7 @@ list_of_tuples = reg.ids_for('london', country='GB', matching='like') # We'll g # (2643734, 'Londonderry County Borough', 'GB')] ``` -### Get geographic coordinates of a city given its name +#### Get geographic coordinates of a city given its name Just use call `locations_for` on the registry: this will give you a `Location` object containing lat & lon Let's find geocoords for Moscow (Russia): @@ -197,7 +203,7 @@ lat = moscow.lat # 55.75222 lon = moscow.lon # 37.615555 ``` -### Get GeoJSON geometry (point) for a city given its name +#### Get GeoJSON geometry (point) for a city given its name PyOWM encapsulates [GeoJSON](https://pypi.org/project/geojson/) geometry objects that are compliant with the GeoJSON specification. This means, for example, that you can get a `Point` geometry using the registry. Let's find the geometries for all `Rome` cities in the world: @@ -209,6 +215,65 @@ reg = owm.city_id_registry() list_of_geopoints = reg.geopoints_for('rome') ``` +### Direct/reverse geocoding + +Simply put: + - DIRECT GEOCODING: from toponym to geocoords + - REVERSE GEOCODING: from geocoords to toponyms + + +Both geocoding actions are performed via a `geocoding_manager` object and will require an actual call to be made to the +OWM API: so please bear that in mind because that will count against your amount of allowed API calls + +#### Direct gecocoding of a toponym + +The call is very similar to `ids_for` and `locations_for`. + +You at least need to specify the toponym name and country ISO code (eg. `GB`, `IT`, `JP`, ...), while if the input +toponym is in the United States you should also specify the `state_code` parameter + +The call returns a list of `Location` object instances (in case of no ambiguity, only one item in the list will be returned) +You can then get the lat/lon from the object instances themselves + +Results can be limited with the `limit` parameter + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.geocoding_manager() + +# geocode London (no country specified) - we'll get many results +list_of_locations = mgr.geocode('London') +a_london = list_of_locations[0] # taking the first London in the list +a_london.lat +a_london.lon + +# geocode London (Great Britain) - we'll get up to three Londons that exist in GB +list_of_locations = mgr.geocode('London', country='GB', limit=3) + +# geocode London (Ohio, United States of America): we'll get all the Londons in Ohio +list_of_locations = mgr.geocode('London', country='US', state_code='OH') +``` + +#### Reverse gecocoding of geocoordinates +With reverse geocoding you input a lat/lon float couple and retrieve a list all the `Location` objects associated with +these coordinates. + +Results can be limited with the `limit` parameter + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.geocoding_manager() + +# London +lat = 51.5098 +lon = -0.1180 + +# reverse geocode London +list_of_locations = mgr.reverse_geocode(lat, lon) # list contains: City of London, Islington, Lewisham, ... +``` +
diff --git a/sphinx/v3/geocoding-api-usage-examples.md b/sphinx/v3/geocoding-api-usage-examples.md new file mode 100644 index 00000000..b82b1b44 --- /dev/null +++ b/sphinx/v3/geocoding-api-usage-examples.md @@ -0,0 +1,8 @@ +# Geocoding API usage examples + +The OWM Weather API gives you the possibility to perform direct and reverse geocoding: + - DIRECT GEOCODING: from toponym to geocoords + - REVERSE GEOCODING: from geocoords to toponyms + + +Please refer to the `Code Recipes` page, sections: `Direct/reverse geocoding` diff --git a/tests/integration/geocodingapi10/__init__.py b/tests/integration/geocodingapi10/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/geocodingapi10/test_integration_geocodingapi10.py b/tests/integration/geocodingapi10/test_integration_geocodingapi10.py new file mode 100644 index 00000000..5e12d93d --- /dev/null +++ b/tests/integration/geocodingapi10/test_integration_geocodingapi10.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import unittest +import os +from pyowm import owm +from pyowm.weatherapi25.location import Location + + +class IntegrationTestsGeocodingAPI(unittest.TestCase): + + __owm = owm.OWM(os.getenv('OWM_API_KEY', None)) + + def test_geocode(self): + mgr = self.__owm.geocoding_manager() + + # Geocode all Paris in the United States + locations = mgr.geocode('Paris', 'US') + self.assertTrue(isinstance(locations, list)) + self.assertTrue(all([isinstance(l, Location) for l in locations])) + self.assertTrue(all([l.name == 'Paris' and l.country == 'US' for l in locations])) + + def test_reverse_geocode(self): + mgr = self.__owm.geocoding_manager() + + # Reverse geocode the geocoords for Florence (Italy) + locations = mgr.reverse_geocode(43.783731, 11.246603) + self.assertTrue(isinstance(locations, list)) + self.assertTrue(all([isinstance(l, Location) for l in locations])) + self.assertTrue(all([l.name == 'Firenze' and l.country == 'IT' for l in locations])) + diff --git a/tests/unit/geocodingapi10/__init__.py b/tests/unit/geocodingapi10/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/geocodingapi10/geocoding_manager.py b/tests/unit/geocodingapi10/geocoding_manager.py new file mode 100644 index 00000000..1bde58a9 --- /dev/null +++ b/tests/unit/geocodingapi10/geocoding_manager.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import json +import unittest +from pyowm.commons.http_client import HttpClient +from pyowm.config import DEFAULT_CONFIG +from pyowm.constants import GEOCODING_API_VERSION +from pyowm.geocodingapi10.geocoding_manager import GeocodingManager +from pyowm.weatherapi25.location import Location + + +class TestGeocodingManager(unittest.TestCase): + + __test_instance = GeocodingManager('fakeapikey', DEFAULT_CONFIG) + + DIRECT_GEOCODING_JSON = '''[{"name":"London","local_names":{"af":"Londen","ar":"لندن","ascii":"London","az":"London","bg":"Лондон","ca":"Londres","da":"London","de":"London","el":"Λονδίνο","en":"London","eu":"Londres","fa":"لندن","feature_name":"London","fi":"Lontoo","fr":"Londres","gl":"Londres","he":"לונדון","hi":"लंदन","hr":"London","hu":"London","id":"London","it":"Londra","ja":"ロンドン","la":"Londinium","lt":"Londonas","mk":"Лондон","nl":"Londen","no":"London","pl":"Londyn","pt":"Londres","ro":"Londra","ru":"Лондон","sk":"Londýn","sl":"London","sr":"Лондон","th":"ลอนดอน","tr":"Londra","vi":"Luân Đôn","zu":"ILondon"},"lat":51.5085,"lon":-0.1257,"country":"GB"},{"name":"London","local_names":{"ar":"لندن","ascii":"London","bg":"Лондон","de":"London","en":"London","fa":"لندن، انتاریو","feature_name":"London","fi":"London","fr":"London","he":"לונדון","ja":"ロンドン","lt":"Londonas","nl":"London","pl":"London","pt":"London","ru":"Лондон","sr":"Лондон"},"lat":42.9834,"lon":-81.233,"country":"CA"},{"name":"London","local_names":{"ar":"لندن","ascii":"London","en":"London","fa":"لندن، اوهایو","feature_name":"London","sr":"Ландон"},"lat":39.8865,"lon":-83.4483,"country":"US","state":"OH"},{"name":"London","local_names":{"ar":"لندن","ascii":"London","en":"London","fa":"لندن، کنتاکی","feature_name":"London","sr":"Ландон"},"lat":37.129,"lon":-84.0833,"country":"US","state":"KY"},{"name":"London","local_names":{"ascii":"London","ca":"Londres","en":"London","feature_name":"London"},"lat":36.4761,"lon":-119.4432,"country":"US","state":"CA"}]''' + REVERSE_GEOCODING_JSON = '''[{"name":"London","local_names":{"af":"Londen","ar":"لندن","ascii":"London","az":"London","bg":"Лондон","ca":"Londres","da":"London","de":"London","el":"Λονδίνο","en":"London","eu":"Londres","fa":"لندن","feature_name":"London","fi":"Lontoo","fr":"Londres","gl":"Londres","he":"לונדון","hi":"लंदन","hr":"London","hu":"London","id":"London","it":"Londra","ja":"ロンドン","la":"Londinium","lt":"Londonas","mk":"Лондон","nl":"Londen","no":"London","pl":"Londyn","pt":"Londres","ro":"Londra","ru":"Лондон","sk":"Londýn","sl":"London","sr":"Лондон","th":"ลอนดอน","tr":"Londra","vi":"Luân Đôn","zu":"ILondon"},"lat":51.5085,"lon":-0.1257,"country":"GB"},{"name":"City of Westminster","local_names":{"ascii":"City of Westminster","feature_name":"City of Westminster"},"lat":51.5,"lon":-0.1167,"country":"GB"},{"name":"Lambeth","local_names":{"ascii":"Lambeth","en":"Lambeth","feature_name":"Lambeth"},"lat":51.4963,"lon":-0.1115,"country":"GB"},{"name":"Clerkenwell","local_names":{"ascii":"Clerkenwell","feature_name":"Clerkenwell","hi":"क्लर्कनवेल","ru":"Кларкенуэлл"},"lat":51.5244,"lon":-0.1102,"country":"GB"},{"name":"City of London","local_names":{"ar":"مدينة لندن","ascii":"City of London","bg":"Сити","ca":"La City","de":"London City","el":"Σίτι του Λονδίνου","en":"City of London","fa":"سیتی لندن","feature_name":"City of London","fi":"Lontoon City","fr":"Cité de Londres","gl":"Cidade de Londres","he":"הסיטי של לונדון","hi":"सिटी ऑफ़ लंदन","id":"Kota London","it":"Londra","ja":"シティ・オブ・ロンドン","la":"Civitas Londinium","lt":"Londono Sitis","pt":"Cidade de Londres","ru":"Сити","sr":"Сити","th":"นครลอนดอน","tr":"Londra Şehri","vi":"Thành phố Luân Đôn","zu":"Idolobha weLondon"},"lat":51.5128,"lon":-0.0918,"country":"GB"}]''' + MALFORMED_JSON = '{"a":"2016-10-01T13:07:01Z","b":[]}' + + def mock_get_json_for_direct_geocoding(self, URI, params): + return 200, json.loads(self.DIRECT_GEOCODING_JSON) + + def mock_get_json_returning_malformed_json(self, URI, params): + return 200, json.loads(self.MALFORMED_JSON) + + def mock_get_json_for_reverse_geocoding(self, URI, params): + return 200, json.loads(self.REVERSE_GEOCODING_JSON) + + def test_geocoding_api_version(self): + result = self.__test_instance.geocoding_api_version() + self.assertIsInstance(result, tuple) + self.assertEqual(result, GEOCODING_API_VERSION) + + def test_instantiation_with_wrong_params(self): + self.assertRaises(AssertionError, GeocodingManager, None, dict()) + self.assertRaises(AssertionError, GeocodingManager, 'apikey', None) + + def test_geocode_with_wrong_params(self): + self.assertRaises(AssertionError, + GeocodingManager.geocode, self.__test_instance, + None) + self.assertRaises(ValueError, + GeocodingManager.geocode, self.__test_instance, + 'London', 'tooooomany') + self.assertRaises(ValueError, + GeocodingManager.geocode, self.__test_instance, + 'London', 'GB', 'tooooomany') + self.assertRaises(AssertionError, + GeocodingManager.geocode, self.__test_instance, + 'London', 'OH', 'US', 'notastring') + self.assertRaises(AssertionError, + GeocodingManager.geocode, self.__test_instance, + 'London', 'OH', 'US', -6) + + def test_geocode_fails(self): + ref_to_original = HttpClient.get_json + HttpClient.get_json = self.mock_get_json_returning_malformed_json + self.assertRaises(Exception, + GeocodingManager.geocode, self.__test_instance, + 'London', 'GB') + HttpClient.get_json = ref_to_original + + def test_geocode(self): + ref_to_original = HttpClient.get_json + HttpClient.get_json = self.mock_get_json_for_direct_geocoding + locations = self.__test_instance.geocode('London', 'GB') + self.assertTrue(isinstance(locations, list)) + self.assertTrue(all([isinstance(l, Location) for l in locations])) + self.assertTrue(all([l.name == 'London' for l in locations])) + HttpClient.get_json = ref_to_original + + def test_reverse_geocode_with_wrong_params(self): + self.assertRaises(AssertionError, + GeocodingManager.reverse_geocode, self.__test_instance, + None, None) + self.assertRaises(AssertionError, + GeocodingManager.reverse_geocode, self.__test_instance, + None, -0.15) + self.assertRaises(AssertionError, + GeocodingManager.reverse_geocode, self.__test_instance, + 42, None) + self.assertRaises(ValueError, + GeocodingManager.reverse_geocode, self.__test_instance, + 167, 15) + self.assertRaises(ValueError, + GeocodingManager.reverse_geocode, self.__test_instance, + 15, 234) + self.assertRaises(AssertionError, + GeocodingManager.reverse_geocode, self.__test_instance, + 42, 16, 'notanint') + self.assertRaises(AssertionError, + GeocodingManager.reverse_geocode, self.__test_instance, + 42, 16, -4) + + def test_reverse_geocode_fails(self): + ref_to_original = HttpClient.get_json + HttpClient.get_json = self.mock_get_json_returning_malformed_json + self.assertRaises(Exception, + GeocodingManager.reverse_geocode, self.__test_instance, + 51.5098, -0.1180) + HttpClient.get_json = ref_to_original + + def test_reverse_geocode(self): + lat = 51.5098 + lon = -0.1180 + ref_to_original = HttpClient.get_json + HttpClient.get_json = self.mock_get_json_for_reverse_geocoding + locations = self.__test_instance.reverse_geocode(lat, lon) + self.assertTrue(isinstance(locations, list)) + self.assertTrue(all([isinstance(l, Location) for l in locations])) + HttpClient.get_json = ref_to_original diff --git a/tests/unit/test_owm.py b/tests/unit/test_owm.py index 5ee9ca13..0603e95a 100644 --- a/tests/unit/test_owm.py +++ b/tests/unit/test_owm.py @@ -7,6 +7,7 @@ from pyowm.airpollutionapi30.airpollution_manager import AirPollutionManager from pyowm.alertapi30.alert_manager import AlertManager from pyowm.commons.cityidregistry import CityIDRegistry +from pyowm.geocodingapi10.geocoding_manager import GeocodingManager from pyowm.stationsapi30.stations_manager import StationsManager from pyowm.tiles.tile_manager import TileManager from pyowm.uvindexapi30.uvindex_manager import UVIndexManager @@ -80,3 +81,8 @@ def test_tile_manager(self): result = self.__test_instance.tile_manager('test') self.assertIsNotNone(result) self.assertIsInstance(result, TileManager) + + def test_geocoding_manager(self): + result = self.__test_instance.geocoding_manager() + self.assertIsNotNone(result) + self.assertIsInstance(result, GeocodingManager) From 522ab332a1f7057e9b0c35747aebc99af9f7220e Mon Sep 17 00:00:00 2001 From: csparpa Date: Fri, 5 Feb 2021 20:05:50 +0100 Subject: [PATCH 29/36] updated as per #369 --- Pipfile.lock | 446 +++++++++++++++++++++++++++++---------------------- 1 file changed, 255 insertions(+), 191 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 68df2c37..c073a2e1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,17 +16,18 @@ "default": { "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "geojson": { "hashes": [ @@ -41,6 +42,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "pysocks": { @@ -53,19 +55,23 @@ "version": "==1.7.1" }, "requests": { + "extras": [ + "socks" + ], "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], "index": "pypi", - "version": "==2.24.0" + "version": "==2.25.1" }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "version": "==1.25.11" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.3" } }, "develop": { @@ -85,85 +91,93 @@ }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==20.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "babel": { "hashes": [ - "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", - "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" + "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", + "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], - "version": "==2.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0" }, "bleach": { "hashes": [ "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", + "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", + "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433", "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], - "version": "==3.2.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.3.0" }, "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "cffi": { "hashes": [ - "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", - "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", - "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", - "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", - "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", - "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", - "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", - "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", - "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", - "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", - "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", - "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", - "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", - "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", - "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", - "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", - "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", - "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", - "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", - "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", - "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", - "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", - "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", - "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", - "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", - "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", - "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", - "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", - "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", - "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", - "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", - "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", - "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", - "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", - "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", - "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" - ], - "version": "==1.14.3" + "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", + "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", + "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", + "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", + "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", + "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", + "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", + "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", + "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", + "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", + "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", + "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", + "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", + "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", + "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", + "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", + "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e", + "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", + "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", + "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", + "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", + "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", + "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", + "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", + "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", + "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", + "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", + "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", + "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", + "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", + "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", + "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", + "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", + "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", + "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", + "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", + "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" + ], + "version": "==1.14.4" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "colorama": { "hashes": [ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, "commonmark": { @@ -175,79 +189,86 @@ }, "coverage": { "hashes": [ - "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", - "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", - "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", - "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", - "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", - "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", - "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", - "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", - "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", - "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", - "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", - "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", - "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", - "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", - "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", - "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", - "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", - "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", - "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", - "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", - "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", - "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", - "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", - "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", - "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", - "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", - "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", - "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", - "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", - "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", - "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", - "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", - "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", - "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", + "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", + "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", + "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", + "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", + "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", + "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", + "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", + "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", + "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", + "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", + "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", + "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", + "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", + "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", + "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", + "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", + "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", + "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", + "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", + "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", + "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", + "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", + "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", + "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", + "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", + "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", + "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", + "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", + "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", + "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", + "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", + "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", + "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", + "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", + "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", + "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", + "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", + "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", + "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", + "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", + "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", + "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", + "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", + "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", + "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", + "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", + "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", + "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" ], "index": "pypi", - "version": "==5.3" + "version": "==5.4" }, "coveralls": { "hashes": [ - "sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6", - "sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1" + "sha256:5399c0565ab822a70a477f7031f6c88a9dd196b3de2877b3facb43b51bd13434", + "sha256:f8384968c57dee4b7133ae701ecdad88e85e30597d496dcba0d7fbb470dca41f" ], "index": "pypi", - "version": "==2.1.2" + "version": "==3.0.0" }, "cryptography": { "hashes": [ - "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", - "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", - "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", - "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", - "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", - "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", - "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", - "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", - "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", - "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", - "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", - "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", - "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", - "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", - "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", - "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", - "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", - "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", - "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", - "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", - "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", - "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" - ], - "index": "pypi", - "version": "==3.2" + "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d", + "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7", + "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901", + "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c", + "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244", + "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6", + "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5", + "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e", + "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c", + "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0", + "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812", + "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a", + "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030", + "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==3.3.1" }, "distlib": { "hashes": [ @@ -267,6 +288,7 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "filelock": { @@ -281,6 +303,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -288,6 +311,7 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "iniconfig": { @@ -299,26 +323,27 @@ }, "jeepney": { "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" + "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", + "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" ], "markers": "sys_platform == 'linux'", - "version": "==0.4.3" + "version": "==0.6.0" }, "jinja2": { "hashes": [ - "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", - "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], "index": "pypi", - "version": "==2.11.2" + "version": "==2.11.3" }, "keyring": { "hashes": [ - "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", - "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" + "sha256:9acb3e1452edbb7544822b12fd25459078769e560fa51f418b6d00afaa6178df", + "sha256:9f44660a5d4931bdc14c08a1d01ef30b18a7a8147380710d8c9f9531e1f6c3c0" ], - "version": "==21.4.0" + "markers": "python_version >= '3.6'", + "version": "==22.0.1" }, "markupsafe": { "hashes": [ @@ -327,8 +352,12 @@ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", @@ -337,98 +366,120 @@ "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "packaging": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "version": "==20.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" }, "pkginfo": { "hashes": [ - "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", - "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" + "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", + "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" ], - "version": "==1.6.1" + "version": "==1.7.0" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "pproxy": { "hashes": [ - "sha256:c6367a5abcae46b4385439772ebfb2a72557fd2675c1c117d0b3038b79e41472", - "sha256:f9dea5f1192adf166e7857320f328349f3f158cf6511ccbac0c7c8c09d864ef5" + "sha256:747aaf8d9ea858cb0a81ec5d6d223abba927879fd8798f7dde54351fc2ee8e49", + "sha256:dfb336a3e794b9a3d06cc95ae94e24f37b28c46765e0ba518015b94d8944782a" ], "index": "pypi", - "version": "==2.3.7" + "version": "==2.5.6" }, "py": { "hashes": [ - "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", - "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "version": "==1.9.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" }, "pycparser": { "hashes": [ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygments": { "hashes": [ - "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", - "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" + "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", + "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" ], - "version": "==2.7.2" + "markers": "python_version >= '3.5'", + "version": "==2.7.4" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33", - "sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7" + "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", + "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" ], "index": "pypi", - "version": "==6.1.0" + "version": "==6.2.2" }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2020.1" + "version": "==2021.1" }, "readme-renderer": { "hashes": [ @@ -439,19 +490,22 @@ }, "recommonmark": { "hashes": [ - "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb", - "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852" + "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f", + "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.7.1" }, "requests": { + "extras": [ + "socks" + ], "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], "index": "pypi", - "version": "==2.24.0" + "version": "==2.25.1" }, "requests-toolbelt": { "hashes": [ @@ -469,33 +523,34 @@ }, "secretstorage": { "hashes": [ - "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", - "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" + "sha256:30cfdef28829dad64d6ea1ed08f8eff6aa115a77068926bcc9f5225d5a3246aa", + "sha256:5c36f6537a523ec5f969ef9fad61c98eb9e017bc601d811e53aa25bece64892f" ], "markers": "sys_platform == 'linux'", - "version": "==3.1.2" + "version": "==3.3.0" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { "hashes": [ - "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", - "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "sphinx": { "hashes": [ - "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", - "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0" + "sha256:41cad293f954f7d37f803d97eb184158cfd90f51195131e94875bc07cd08b93c", + "sha256:c314c857e7cd47c856d2c5adff514ac2e6495f8b8e0f886a8a37e9305dfea0d8" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.4.3" }, "sphinx-readable-theme": { "hashes": [ @@ -509,6 +564,7 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -516,6 +572,7 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -523,6 +580,7 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -530,6 +588,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -537,6 +596,7 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -544,22 +604,24 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], + "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" }, "tox": { "hashes": [ - "sha256:e6318f404aff16522ff5211c88cab82b39af121735a443674e4e2e65f4e4637b", - "sha256:eb629ddc60e8542fd4a1956b2462e3b8771d49f1ff630cecceacaa0fbfb7605a" + "sha256:65d0e90ceb816638a50d64f4b47b11da767b284c0addda2294cb3cd69bd72425", + "sha256:cf7fef81a3a2434df4d7af2a6d1bf606d2970220addfbe7dea2615bd4bb2c252" ], "index": "pypi", - "version": "==3.20.0" + "version": "==3.21.4" }, "tox-travis": { "hashes": [ @@ -571,33 +633,35 @@ }, "tqdm": { "hashes": [ - "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", - "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" + "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a", + "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65" ], - "version": "==4.51.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.56.0" }, "twine": { "hashes": [ - "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", - "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" + "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", + "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" ], "index": "pypi", - "version": "==3.2.0" + "version": "==3.3.0" }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "version": "==1.25.11" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.3" }, "virtualenv": { "hashes": [ - "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", - "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" + "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", + "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" ], "index": "pypi", - "version": "==20.0.31" + "version": "==20.4.2" }, "webencodings": { "hashes": [ @@ -607,4 +671,4 @@ "version": "==0.5.1" } } -} \ No newline at end of file +} From b29378679884dfc15604b4c595d32e4096393926 Mon Sep 17 00:00:00 2001 From: Crozzers Date: Wed, 17 Feb 2021 20:36:09 +0000 Subject: [PATCH 30/36] Fix typos (#372) * Fixed some typos * More spell checking with aspell --- CONTRIBUTORS.md | 1 + pyowm/agroapi10/search.py | 2 +- pyowm/commons/tile.py | 2 +- pyowm/utils/formatting.py | 2 +- pyowm/weatherapi25/historian.py | 16 ++++++++-------- sphinx/contributing.md | 10 +++++----- sphinx/index.rst | 2 +- sphinx/v3/agro-api-usage-examples.md | 6 +++--- sphinx/v3/air-pollution-api-usage-examples.md | 6 +++--- sphinx/v3/alerts-api-usage-examples.md | 6 +++--- sphinx/v3/city-id-registry-examples.md | 2 +- sphinx/v3/code-recipes.md | 8 ++++---- sphinx/v3/faq.md | 2 +- sphinx/v3/global-pyowm-usage-examples.md | 2 +- sphinx/v3/stations-api-usage-examples.md | 2 +- 15 files changed, 35 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 91c0f8c0..511a0559 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -30,6 +30,7 @@ Code Docs ---- + * [Crozzers](https://github.com/Crozzers) * [EJEP](https://github.com/EJEP) * [Franzqat](https://github.com/franzqat) * [Harmon758](https://github.com/Harmon758) diff --git a/pyowm/agroapi10/search.py b/pyowm/agroapi10/search.py index 57c77af2..6d809542 100644 --- a/pyowm/agroapi10/search.py +++ b/pyowm/agroapi10/search.py @@ -180,7 +180,7 @@ def with_img_type(self, image_type): def with_preset(self, preset): """ - Returns the seach results having the specified preset + Returns the search results having the specified preset :param preset: the desired image preset (valid values are provided by the `pyowm.agroapi10.enums.PresetEnum` enum) diff --git a/pyowm/commons/tile.py b/pyowm/commons/tile.py index e219e91e..37a4b0b0 100644 --- a/pyowm/commons/tile.py +++ b/pyowm/commons/tile.py @@ -92,7 +92,7 @@ def geoocoords_to_tile_coords(cls, lon, lat, zoom): @classmethod def tile_coords_to_bbox(cls, x, y, zoom): """ - Calculates the lon/lat estrema of the bounding box corresponding to specific tile coordinates. Output coodinates + Calculates the lon/lat estrema of the bounding box corresponding to specific tile coordinates. Output coordinates are in degrees and in the Mercator Projection (http://en.wikipedia.org/wiki/Mercator_projection) :param x: the x tile coordinates diff --git a/pyowm/utils/formatting.py b/pyowm/utils/formatting.py index 7d167f87..c0dc3c5d 100644 --- a/pyowm/utils/formatting.py +++ b/pyowm/utils/formatting.py @@ -134,7 +134,7 @@ def to_UNIXtime(timeobject): def ISO8601_to_UNIXtime(iso): """ Converts an ISO8601-formatted string in the format - ``YYYY-MM-DD HH:MM:SS+00:00`` to the correspondant UNIXtime + ``YYYY-MM-DD HH:MM:SS+00:00`` to the correspondent UNIXtime :param iso: the ISO8601-formatted string :type iso: string diff --git a/pyowm/weatherapi25/historian.py b/pyowm/weatherapi25/historian.py index cc12b6a0..2ebd5fc0 100644 --- a/pyowm/weatherapi25/historian.py +++ b/pyowm/weatherapi25/historian.py @@ -92,7 +92,7 @@ def wind_series(self): def max_temperature(self, unit='kelvin'): """Returns a tuple containing the max value in the temperature - series preceeded by its timestamp + series preceded by its timestamp :param unit: the unit of measure for the temperature values. May be among: '*kelvin*' (default), '*celsius*' or '*fahrenheit*' @@ -114,7 +114,7 @@ def max_temperature(self, unit='kelvin'): def min_temperature(self, unit='kelvin'): """Returns a tuple containing the min value in the temperature - series preceeded by its timestamp + series preceded by its timestamp :param unit: the unit of measure for the temperature values. May be among: '*kelvin*' (default), '*celsius*' or '*fahrenheit*' @@ -157,7 +157,7 @@ def average_temperature(self, unit='kelvin'): def max_humidity(self): """Returns a tuple containing the max value in the humidity - series preceeded by its timestamp + series preceded by its timestamp :returns: a tuple :raises: ValueError when the measurement series is empty @@ -167,7 +167,7 @@ def max_humidity(self): def min_humidity(self): """Returns a tuple containing the min value in the humidity - series preceeded by its timestamp + series preceded by its timestamp :returns: a tuple :raises: ValueError when the measurement series is empty @@ -186,7 +186,7 @@ def average_humidity(self): def max_pressure(self): """Returns a tuple containing the max value in the pressure - series preceeded by its timestamp + series preceded by its timestamp :returns: a tuple :raises: ValueError when the measurement series is empty @@ -196,7 +196,7 @@ def max_pressure(self): def min_pressure(self): """Returns a tuple containing the min value in the pressure - series preceeded by its timestamp + series preceded by its timestamp :returns: a tuple :raises: ValueError when the measurement series is empty @@ -215,7 +215,7 @@ def average_pressure(self): def max_rain(self): """Returns a tuple containing the max value in the rain - series preceeded by its timestamp + series preceded by its timestamp :returns: a tuple :raises: ValueError when the measurement series is empty @@ -225,7 +225,7 @@ def max_rain(self): def min_rain(self): """Returns a tuple containing the min value in the rain - series preceeded by its timestamp + series preceded by its timestamp :returns: a tuple :raises: ValueError when the measurement series is empty diff --git a/sphinx/contributing.md b/sphinx/contributing.md index 13527a21..2df8bc94 100644 --- a/sphinx/contributing.md +++ b/sphinx/contributing.md @@ -1,6 +1,6 @@ # Contributing -Contributing is easy anwd welcome +Contributing is easy and welcome You can contribute to PyOWM in a lot of ways: @@ -8,7 +8,7 @@ You can contribute to PyOWM in a lot of ways: - make a wish for a reasonable new feature - increase the test coverage - refactor the code - - improve PyOWM reach on platforms (eg. bundle it for Linux distros, managers, oding, testing, packaging, reporting issues) are welcome! + - improve PyOWM reach on platforms (eg. bundle it for Linux distros, managers, coding, testing, packaging, reporting issues) are welcome! And last but not least... use it! Use PyOWM in your own projects, as [lots of people already do](https://github.com/csparpa/pyowm/wiki/Community-Projects-using-PyOWM). @@ -26,9 +26,9 @@ That's simple: what you need to do is just open a new issue on GitHub. ## Bug reports - general principles In order to allow the community to understand what the bug is, *you should provide as much information as possible* on it. -Vague or succint bug reports are not useful and will very likely result in follow ups needed. +Vague or succinct bug reports are not useful and will very likely result in follow ups needed. -*Only bugs related to PyOWM will be addressed*: it might be that you're using PyOWM in a broader context (eg. a webapplication) +*Only bugs related to PyOWM will be addressed*: it might be that you're using PyOWM in a broader context (eg. a web application) so bugs affecting the broader context are out of scope - unless they are caused in chain to PyOWM issues. Also, please do understand that we can only act on *reproducible bugs*: this means that a bug does not exist if it is @@ -74,7 +74,7 @@ just run: `pip install -r dev-requirements.txt` -It is adviced that you do it on a [virtualenv](https://virtualenv.pypa.io/en/stable/). +It is advised that you do it on a [virtualenv](https://virtualenv.pypa.io/en/stable/). ## Guidelines for code branching Simple ones: diff --git a/sphinx/index.rst b/sphinx/index.rst index 52897038..2890f840 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -239,7 +239,7 @@ If you already have PyOWM 2.x installed and want to upgrade to safely update it $ pip install --upgrade pyowm>=2.0,<3.0 -Get the lastest development version +Get the latest development version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can install the development trunk with _pip_: diff --git a/sphinx/v3/agro-api-usage-examples.md b/sphinx/v3/agro-api-usage-examples.md index 9fb09d9d..b0f6674b 100644 --- a/sphinx/v3/agro-api-usage-examples.md +++ b/sphinx/v3/agro-api-usage-examples.md @@ -32,7 +32,7 @@ Read on to discover what you can do with it. A polygon represents an area on a map upon which you can issue data queries. Each polygon has a unique ID, an optional name and links back to unique OWM ID of the User that owns that polygon. Each polygon has an area that is expressed -in hacres, but you can also get it in squared kilometers: +in acres, but you can also get it in squared kilometers: ```python pol # this is a pyowm.agro10.polygon.Polygon instance @@ -133,7 +133,7 @@ Soil data is updated twice a day. This is the real meat in Agro API: the possibility to obtain **satellite imagery** right upon your polygons! -### Overwiew +### Overview Satellite Imagery comes in 3 formats: - **PNG images** @@ -416,7 +416,7 @@ Currently only PNG and GEOTIFF imagery is available ### Supported color palettes Supported color palettes are provided by the `pyowm.agroapi10.enums.PaletteEnum` enumerator which returns strings, -each one representing a color palette for NDVI imges: +each one representing a color palette for NDVI images: ```python from pyowm.agroapi10.enums import PaletteEnum diff --git a/sphinx/v3/air-pollution-api-usage-examples.md b/sphinx/v3/air-pollution-api-usage-examples.md index 5f11e7d7..3c346f26 100644 --- a/sphinx/v3/air-pollution-api-usage-examples.md +++ b/sphinx/v3/air-pollution-api-usage-examples.md @@ -59,7 +59,7 @@ max_sample = coi.get_co_sample_with_highest_vmr() min_sample = coi.get_co_sample_with_lowest_vmr() ``` -If you want to know if a COIndex refers to the future - aka: is a forecast - wth respect to the +If you want to know if a COIndex refers to the future - aka: is a forecast - with respect to the current timestamp, then use the `is_forecast()` method @@ -119,9 +119,9 @@ oz.get_reference_time() oz.get_reception_time() ``` -If you want to know if an Ozone measurement refers to the future - aka: is a forecast - wth respect to the +If you want to know if an Ozone measurement refers to the future - aka: is a forecast - with respect to the current timestamp, then use the `is_forecast()` method ### Querying Nitrogen dioxide (NO2) and Sulfur Dioxide (SO2) data -This works exactly as for O2 adata - please refer to that bit of the docs \ No newline at end of file +This works exactly as for O2 data - please refer to that bit of the docs \ No newline at end of file diff --git a/sphinx/v3/alerts-api-usage-examples.md b/sphinx/v3/alerts-api-usage-examples.md index 3f5af6ee..fe57edd5 100644 --- a/sphinx/v3/alerts-api-usage-examples.md +++ b/sphinx/v3/alerts-api-usage-examples.md @@ -89,7 +89,7 @@ am.delete_alert_for(trigger, alert) This is the Alert API object model: - *Trigger*: collection of alerts to be met over specified areas and within a specified time frame according to specified weather params conditions - - *Condition*: rule for matching a weather measuerment with a specified threshold + - *Condition*: rule for matching a weather measurement with a specified threshold - *Alert*: whenever a condition is met, an alert is created (or updated) and can be polled to verify when it has been met and what the actual weather param value was. - *Area*: geographic area over which the trigger is checked - *AlertChannel*: as OWM plans to add push-oriented alert channels (eg. push notifications), we need to encapsulate this into a specific class @@ -283,8 +283,8 @@ A Trigger is the local proxy for the corresponding entry on the OWM API: Trigger `pyowm.alertapi30.alertmanager.AlertManager` instances. Each Trigger has these attributes: - - start_after_millis: _with resepect to the time when the trigger will be crated on the Alert API_, how many milliseconds after should it begin to be checked for conditions matching - - end_after_millis: _with resepect to the time when the trigger will be crated on the Alert API_, how many milliseconds after should it end to be checked for conditions matching + - start_after_millis: _with respect to the time when the trigger will be created on the Alert API_, how many milliseconds after should it begin to be checked for conditions matching + - end_after_millis: _with respect to the time when the trigger will be created on the Alert API_, how many milliseconds after should it end to be checked for conditions matching - alerts: a list of `pyowm.alertapi30.alert.Alert` instances, which are the alerts that the trigger has fired so far - conditions: a list of `pyowm.alertapi30.condition.Condition` instances - area: a list of `pyowm.utils.geo.Geometry` instances, representing the geographic area on which the trigger's conditions need to be checked diff --git a/sphinx/v3/city-id-registry-examples.md b/sphinx/v3/city-id-registry-examples.md index 047dab73..dd919334 100644 --- a/sphinx/v3/city-id-registry-examples.md +++ b/sphinx/v3/city-id-registry-examples.md @@ -2,6 +2,6 @@ Using city IDS instead of toponyms or geographic coordinates is the preferred way of querying the OWM weather API -You can obtain the city ID for your toponyms/geoocoords of interest via the `City ID Registry`. +You can obtain the city ID for your toponyms/geocoords of interest via the `City ID Registry`. Please refer to the `Code Recipes` page, section: `Identifying cities and places via city IDs`, to get info about it \ No newline at end of file diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 8575ae7c..b81a7014 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -148,7 +148,7 @@ city_id_registry = owm.city_id_registry() ``` #### Get the ID of a city given its name -Don't forget that there is a high probabilty that your city is not unique in the world, and multiple cities with the same name exist in other countries +Don't forget that there is a high probability that your city is not unique in the world, and multiple cities with the same name exist in other countries Therefore specify toponyms and country 2-letter names separated by comma. Eg: if you search for the British `London` you'll likely multiple results: you then should also specify the country (`GB`) to narrow the search only to Great Britain. @@ -225,7 +225,7 @@ Simply put: Both geocoding actions are performed via a `geocoding_manager` object and will require an actual call to be made to the OWM API: so please bear that in mind because that will count against your amount of allowed API calls -#### Direct gecocoding of a toponym +#### Direct geocoding of a toponym The call is very similar to `ids_for` and `locations_for`. @@ -255,7 +255,7 @@ list_of_locations = mgr.geocode('London', country='GB', limit=3) list_of_locations = mgr.geocode('London', country='US', state_code='OH') ``` -#### Reverse gecocoding of geocoordinates +#### Reverse geocoding of geocoordinates With reverse geocoding you input a lat/lon float couple and retrieve a list all the `Location` objects associated with these coordinates. @@ -345,7 +345,7 @@ one_call = mgr.one_call(lat=52.5244, lon=13.4105, exclude='minutely,hourly', uni # the various units for the different options are shown here: https://openweathermap.org/weather-data one_call.current.temperature() # Eg.: 74.07 (deg F) -# the example above does not retrieve minutely or hourly data, so it will not be availabe in the one_call object +# the example above does not retrieve minutely or hourly data, so it will not be available in the one_call object # available exclude options are defined by the One Call API # BUT using 'current' will error, as the pyowm one_call requires it # as of 2020.08.07 available values are: 'minutely', 'hourly', 'daily' diff --git a/sphinx/v3/faq.md b/sphinx/v3/faq.md index 1ad9c86d..54998142 100644 --- a/sphinx/v3/faq.md +++ b/sphinx/v3/faq.md @@ -30,7 +30,7 @@ As stated in the documentation home page, OpenWeatherMap API recently "blocked" This means that PyOWM might return authorization errors in that case. -This behaviour is not showing if you use API keys issued time ago - unfortunately I have no wasy to be more precise as OWM never stated this officially. +This behaviour is not showing if you use API keys issued time ago - unfortunately I have no way to be more precise as OWM never stated this officially. **The proper way to obtain the data you are looking for is to call the "OneCall" PyOWM methods using your API key** diff --git a/sphinx/v3/global-pyowm-usage-examples.md b/sphinx/v3/global-pyowm-usage-examples.md index 80025ee7..18f33a00 100644 --- a/sphinx/v3/global-pyowm-usage-examples.md +++ b/sphinx/v3/global-pyowm-usage-examples.md @@ -18,7 +18,7 @@ weather = mgr.weather_at_place('London,GB').weather # get the weather at London dump_dict = weather.to_dict() ``` -This is useful as you can save the dump dictionaries to files (eg. using Pyhon `json` or `pickle` modules) +This is useful as you can save the dump dictionaries to files (eg. using Python `json` or `pickle` modules) ## Printing objects Most of PyOWM objects can be pretty-printed for a quick introspection: diff --git a/sphinx/v3/stations-api-usage-examples.md b/sphinx/v3/stations-api-usage-examples.md index d33a8434..9fc715ff 100644 --- a/sphinx/v3/stations-api-usage-examples.md +++ b/sphinx/v3/stations-api-usage-examples.md @@ -46,7 +46,7 @@ datapoints that you query against the API come in the form of: `stationsapi30.measurement.AggregatedMeasurement` objects. -Each `stationsapi30.measurement.Measurement` cointains a reference to the +Each `stationsapi30.measurement.Measurement` contains a reference to the `Station` it belongs to: ```python From 383361c65092a447100f38d286776ec5334fe712 Mon Sep 17 00:00:00 2001 From: csparpa Date: Wed, 17 Feb 2021 21:48:11 +0100 Subject: [PATCH 31/36] add deprecation disclaimer --- sphinx/v3/code-recipes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 8575ae7c..44eb9592 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -662,6 +662,11 @@ obs_list = mgr.weather_at_places_in_bbox(lon_left, lat_bottom, lon_right, lat_to ## Weather forecasts +**>>>IMPORTANT NOTE<<<**: OpenWeatherMap has deprecated legacy weather forecasts endpoints, therefore you could get +errors if you invoke them +The recommended way to get weather forecasts is now the [*OneCall* API]((#onecall)) + + ### Get forecast on a location Just like for observed weather info, you can fetch weather forecast info on a specific toponym. As usual, provide toponym + country code for better results. From 2933f065cf83b4a229da67a804f019c865b540a2 Mon Sep 17 00:00:00 2001 From: csparpa Date: Wed, 17 Feb 2021 21:58:21 +0100 Subject: [PATCH 32/36] heads up --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 43cd262b..ba7166ff 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ With PyOWM you can integrate into your code any of the following OpenWeatherMap - **Image tiles** for several map layers provided by OWM - **Geocoding API v1.0** allowing to perform direct/reverse geocoding + +## In case of trouble... +Please **read the [FAQ](https://pyowm.readthedocs.io/en/latest/v3/faq.html)** before filing a new issue on GitHub! There are many common issues, therefore a fix for your issue might come easier than you think + ## Get started ### API key @@ -108,11 +112,6 @@ The library software API documentation is available on [Read the Docs](https://p The [Code recipes](https://pyowm.readthedocs.io/en/latest/v3/code-recipes.html) section comes in handy! -## Help - PyOWM is giving me errors! -Please read the [FAQ](https://pyowm.readthedocs.io/en/latest/v3/faq.html) section of the documentation before filing a new issue on GitHub! - -There are many common issues, therefore a fix for your issue might come easier than you think - ## Community & Contributing Here are [some cool projects](https://github.com/csparpa/pyowm/wiki/Community-Projects-using-PyOWM) that use PyOWM From f68d6b7bfaaa633c0fa280af633879ab0374c2cb Mon Sep 17 00:00:00 2001 From: csparpa Date: Tue, 23 Feb 2021 23:59:38 +0100 Subject: [PATCH 33/36] Air pollution forecasts --- .../airpollutionapi30/airpollution_client.py | 17 +++++++- .../airpollutionapi30/airpollution_manager.py | 30 ++++++++++++-- pyowm/airpollutionapi30/airstatus.py | 39 +++++++++++-------- pyowm/airpollutionapi30/uris.py | 1 + sphinx/v3/code-recipes.md | 24 +++++++++++- .../__init__.py | 0 .../test_integration_pollutionapi30.py | 14 ++++++- .../test_airpollution_manager.py | 32 ++++++++++++++- .../unit/airpollutionapi30/test_airstatus.py | 19 ++++++++- 9 files changed, 151 insertions(+), 25 deletions(-) rename tests/integration/{pollutionapi30 => airpollutionapi30}/__init__.py (100%) rename tests/integration/{pollutionapi30 => airpollutionapi30}/test_integration_pollutionapi30.py (51%) diff --git a/pyowm/airpollutionapi30/airpollution_client.py b/pyowm/airpollutionapi30/airpollution_client.py index 07a54a63..71161c82 100644 --- a/pyowm/airpollutionapi30/airpollution_client.py +++ b/pyowm/airpollutionapi30/airpollution_client.py @@ -1,7 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL, AIR_POLLUTION_URL +from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL, AIR_POLLUTION_URL, \ + AIR_POLLUTION_FORECAST_URL from pyowm.utils import formatting @@ -164,6 +165,18 @@ def get_air_pollution(self, params_dict): _, json_data = self._client.get_json(AIR_POLLUTION_URL, params=params_dict) return json_data + def get_forecast_air_pollution(self, params_dict): + """ + Invokes the new AirPollution API forecast endpoint + + :param params_dict: dict of parameters + :returns: a string containing raw JSON data + :raises: *ValueError*, *APIRequestError* + + """ + _, json_data = self._client.get_json(AIR_POLLUTION_FORECAST_URL, params=params_dict) + return json_data + def __repr__(self): return "<%s.%s - httpclient=%s>" % \ - (__name__, self.__class__.__name__, str(self._client)) + (__name__, self.__class__.__name__, str(self._client)) \ No newline at end of file diff --git a/pyowm/airpollutionapi30/airpollution_manager.py b/pyowm/airpollutionapi30/airpollution_manager.py index 0151dffc..4bce9cc4 100644 --- a/pyowm/airpollutionapi30/airpollution_manager.py +++ b/pyowm/airpollutionapi30/airpollution_manager.py @@ -197,7 +197,7 @@ def so2index_around_coords(self, lat, lon, start=None, interval=None): def air_quality_at_coords(self, lat, lon): """ - Queries the OWM AirPollution API for all available air quality indicators around the specified coordinates. + Queries the OWM AirPollution API for available air quality indicators around the specified coordinates. :param lat: the location's latitude, must be between -90.0 and 90.0 :type lat: int/float @@ -212,8 +212,32 @@ def air_quality_at_coords(self, lat, lon): geo.assert_is_lat(lat) params = {'lon': lon, 'lat': lat} json_data = self.new_ap_client.get_air_pollution(params) - air_status = airstatus.AirStatus.from_dict(json_data) - return air_status + try: + return airstatus.AirStatus.from_dict(json_data) + except: + return None + + def air_quality_forecast_at_coords(self, lat, lon): + """ + Queries the OWM AirPollution API for available forecasted air quality indicators around the specified coordinates. + + :param lat: the location's latitude, must be between -90.0 and 90.0 + :type lat: int/float + :param lon: the location's longitude, must be between -180.0 and 180.0 + :type lon: int/float + :return: a `list` of *AirStatus* instances or an empty `list` if data is not available + :raises: *ParseResponseException* when OWM AirPollution API responses' data + cannot be parsed, *APICallException* when OWM AirPollution API can not be + reached, *ValueError* for wrong input values + """ + geo.assert_is_lon(lon) + geo.assert_is_lat(lat) + params = {'lon': lon, 'lat': lat} + json_data = self.new_ap_client.get_forecast_air_pollution(params) + try: + return airstatus.AirStatus.from_dict(json_data) + except: + return [] def __repr__(self): return '<%s.%s>' % (__name__, self.__class__.__name__) diff --git a/pyowm/airpollutionapi30/airstatus.py b/pyowm/airpollutionapi30/airstatus.py index e96a13f0..3602c1b7 100644 --- a/pyowm/airpollutionapi30/airstatus.py +++ b/pyowm/airpollutionapi30/airstatus.py @@ -76,41 +76,48 @@ def reception_time(self, timeformat='unix'): @classmethod def from_dict(cls, the_dict): """ - Parses a *AirStatus* instance out of a data dictionary. + Parses an *AirStatus* instance or `list` of instances out of a data dictionary. :param the_dict: the input dictionary :type the_dict: `dict` - :returns: a *AirStatus* instance or ``None`` if no data is available + :returns: a *AirStatus* instance or ``list` of such instances :raises: *ParseAPIResponseError* if it is impossible to find or parse the data needed to build the result """ if the_dict is None: raise exceptions.ParseAPIResponseError('Data is None') try: - - item = the_dict['list'][0] - - # -- reference time (strip away Z and T on ISO8601 format) - reference_time = item['dt'] - - # -- reception time (now) - reception_time = timestamps.now('unix') - # -- location lon = float(the_dict['coord']['lat']) lat = float(the_dict['coord']['lon']) place = location.Location(None, lon, lat, None) - # -- air quality data - data = item['components'] - data['aqi'] = item['main']['aqi'] + # -- reception time (now) + rcp_time = timestamps.now('unix') + + def build_air_status(item_dict, location, reception_time): + # -- reference time (strip away Z and T on ISO8601 format) + reference_time = item_dict['dt'] + + # -- air quality data + data = item_dict['components'] + data['aqi'] = item_dict['main']['aqi'] + + return AirStatus(reference_time, location, data, reception_time) + + items = the_dict['list'] + + # one datapoint + if len(items) == 1: + return build_air_status(items[0], place, rcp_time) + # multiple datapoints + else: + return [build_air_status(item, place, rcp_time) for item in items] except KeyError: raise exceptions.ParseAPIResponseError( ''.join([__name__, ': impossible to parse AirStatus'])) - return AirStatus(reference_time, place, data, reception_time) - def to_dict(self): """Dumps object to a dictionary diff --git a/pyowm/airpollutionapi30/uris.py b/pyowm/airpollutionapi30/uris.py index cfbd77d9..5df22d8b 100644 --- a/pyowm/airpollutionapi30/uris.py +++ b/pyowm/airpollutionapi30/uris.py @@ -12,3 +12,4 @@ # current API endpoint NEW_ROOT_POLLUTION_API_URL = 'openweathermap.org/data/2.5' AIR_POLLUTION_URL = 'air_pollution' +AIR_POLLUTION_FORECAST_URL = 'air_pollution/forecast' diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 911b0e5a..2b3a6964 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -831,7 +831,7 @@ TBD Instead of getting a `weather_manager`, get from the main OWM object a `airpollution_manager` and use it -### Getting air polluting concentrations and Air Quality Index on geographic coords +### Getting air pollution concentrations and Air Quality Index on geographic coords Air polluting agents concentration can be queried in one shot: ```python @@ -855,6 +855,28 @@ air_status.nh3 air_status.aqi ``` +### Getting forecasts for air pollution on geographic coords +We can get also get forecasts for air pollution agents concentration and air quality index: + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.airpollution_manager() + +list_of_forecasts = mgr.air_quality_forecast_at_coords(51.507351, -0.127758) # London, GB + +# Each item in the list_of_forecasts is an AirStatus object +for air_status in list_of_forecasts: + air_status.co + air_status.no + air_status.no2 + air_status.o3 + air_status.so2 + air_status.pm2_5 + air_status.pm10 + air_status.nh3 + air_status.aqi # air quality index +```
diff --git a/tests/integration/pollutionapi30/__init__.py b/tests/integration/airpollutionapi30/__init__.py similarity index 100% rename from tests/integration/pollutionapi30/__init__.py rename to tests/integration/airpollutionapi30/__init__.py diff --git a/tests/integration/pollutionapi30/test_integration_pollutionapi30.py b/tests/integration/airpollutionapi30/test_integration_pollutionapi30.py similarity index 51% rename from tests/integration/pollutionapi30/test_integration_pollutionapi30.py rename to tests/integration/airpollutionapi30/test_integration_pollutionapi30.py index a789c589..0530b8be 100644 --- a/tests/integration/pollutionapi30/test_integration_pollutionapi30.py +++ b/tests/integration/airpollutionapi30/test_integration_pollutionapi30.py @@ -12,7 +12,7 @@ class IntegrationTestsPollutionAPI30(unittest.TestCase): def test_air_quality_at_coords(self): """ - Test feature: get all air quality data around geo-coordinates. + Test feature: get all air quality data around geo-coordinates. """ airstatus = self.__owm.air_quality_at_coords(45, 9) self.assertIsNotNone(airstatus) @@ -21,6 +21,18 @@ def test_air_quality_at_coords(self): self.assertIsNotNone(airstatus.reference_time()) self.assertIsNotNone(airstatus.location) + def test_air_quality_forecast_at_coords(self): + """ + Test feature: get all forecasted air quality data around geo-coordinates. + """ + list_of_airstatuses = self.__owm.air_quality_forecast_at_coords(45, 9) + self.assertTrue(list_of_airstatuses) + for airstatus in list_of_airstatuses: + self.assertIsNotNone(airstatus.air_quality_data) + self.assertIsNotNone(airstatus.reception_time()) + self.assertIsNotNone(airstatus.reference_time()) + self.assertIsNotNone(airstatus.location) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/airpollutionapi30/test_airpollution_manager.py b/tests/unit/airpollutionapi30/test_airpollution_manager.py index 01f71194..2aea30b5 100644 --- a/tests/unit/airpollutionapi30/test_airpollution_manager.py +++ b/tests/unit/airpollutionapi30/test_airpollution_manager.py @@ -10,7 +10,7 @@ from tests.unit.airpollutionapi30.test_coindex import COINDEX_JSON from tests.unit.airpollutionapi30.test_no2index import NO2INDEX_JSON from tests.unit.airpollutionapi30.test_so2index import SO2INDEX_JSON -from tests.unit.airpollutionapi30.test_airstatus import AIRSTATUS_JSON +from tests.unit.airpollutionapi30.test_airstatus import AIRSTATUS_JSON, AIRSTATUS_MULTIPLE_JSON class TestAirPollutionManager(unittest.TestCase): @@ -29,6 +29,9 @@ def mock_get_no2_returning_no2index_around_coords(self, params_dict): def mock_get_air_pollution(self, params_dict): return json.loads(AIRSTATUS_JSON) + def mock_get_forecast_air_pollution(self, params_dict): + return json.loads(AIRSTATUS_MULTIPLE_JSON) + def mock_get_so2_returning_so2index_around_coords(self, params_dict): return json.loads(SO2INDEX_JSON) @@ -199,5 +202,32 @@ def test_air_quality_at_coords_fails_with_wrong_parameters(self): self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_at_coords, \ self.__test_instance, 200, 2.5) + def test_air_quality_forecast_at_coords(self): + ref_to_original = airpollution_client.AirPollutionHttpClient.get_forecast_air_pollution + airpollution_client.AirPollutionHttpClient.get_forecast_air_pollution = \ + self.mock_get_forecast_air_pollution + result = self.__test_instance.air_quality_forecast_at_coords(45, 9) + airpollution_client.AirPollutionHttpClient.get_forecast_air_pollution = ref_to_original + self.assertTrue(isinstance(result, list)) + for item in result: + self.assertIsInstance(item, airstatus.AirStatus) + self.assertIsNotNone(item.reference_time) + self.assertIsNotNone(item.reception_time()) + loc = item.location + self.assertIsNotNone(loc) + self.assertIsNotNone(loc.lat) + self.assertIsNotNone(loc.lon) + self.assertIsNotNone(item.air_quality_data) + + def test_air_quality_forecast_at_coords_fails_with_wrong_parameters(self): + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_forecast_at_coords, \ + self.__test_instance, 43.7, -200.0) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_forecast_at_coords, \ + self.__test_instance, 43.7, 200.0) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_forecast_at_coords, \ + self.__test_instance, -200, 2.5) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_forecast_at_coords, \ + self.__test_instance, 200, 2.5) + def test_repr(self): print(self.__test_instance) diff --git a/tests/unit/airpollutionapi30/test_airstatus.py b/tests/unit/airpollutionapi30/test_airstatus.py index 66d8b6b1..700ee6bd 100644 --- a/tests/unit/airpollutionapi30/test_airstatus.py +++ b/tests/unit/airpollutionapi30/test_airstatus.py @@ -10,6 +10,7 @@ AIRSTATUS_JSON = '{"coord":{"lon":-0.1278,"lat":51.5074},"list":[{"main":{"aqi":1},"components":{"co":250.34,"no":0.19,"no2":35.99,"o3":30.76,"so2":8.11,"pm2_5":3.15,"pm10":3.81,"nh3":0.74},"dt":1611597600}]}' +AIRSTATUS_MULTIPLE_JSON = '{"coord":{"lon":50,"lat":50},"list":[{"main":{"aqi":1},"components":{"co":240.33,"no":0,"no2":1.07,"o3":79.39,"so2":0.97,"pm2_5":1.84,"pm10":1.9,"nh3":1.25},"dt":1613606400},{"main":{"aqi":1},"components":{"co":240.33,"no":0,"no2":0.98,"o3":79.39,"so2":0.69,"pm2_5":1.92,"pm10":1.97,"nh3":1.36},"dt":1613610000}]}' AIRSTATUS_MALFORMED_JSON = '{"time":"2016-10-01T13:07:01Z","xyz":[]}' AIRSTATUS_JSON_DUMP = '{"reference_time": 1234567, "location": {"name": "test", "coordinates": {"lon": 12.3, "lat": 43.7}, "ID": 987, "country": "UK"}, "air_quality_data": {"aqi": 1, "co": 250.34, "no": 0.19, "no2": 35.99, "o3": 30.76, "so2": 8.11, "pm2_5": 3.15, "pm10": 3.81, "nh3": 0.74}, "reception_time": 1475283600}' @@ -64,11 +65,11 @@ def test_returning_different_formats_for_reception_time(self): self.__test_date_reception_time) def test_from_dict(self): + # one item d = json.loads(AIRSTATUS_JSON) result = AirStatus.from_dict(d) self.assertIsNotNone(result) self.assertIsNotNone(result.reference_time()) - self.assertIsNotNone(result.reference_time()) loc = result.location self.assertIsNotNone(loc) self.assertIsNone(loc.name) @@ -78,6 +79,22 @@ def test_from_dict(self): for key in self.__test_air_quality_data: getattr(result, key) + # multiple items + d = json.loads(AIRSTATUS_MULTIPLE_JSON) + result = AirStatus.from_dict(d) + self.assertIsInstance(result, list) + for item in result: + self.assertIsInstance(item, AirStatus) + self.assertIsNotNone(item.reference_time()) + loc = item.location + self.assertIsNotNone(loc) + self.assertIsNone(loc.name) + self.assertIsNone(loc.id) + self.assertIsNotNone(loc.lon) + self.assertIsNotNone(loc.lat) + for key in self.__test_air_quality_data: + getattr(item, key) + def test_from_dict_fails_when_JSON_data_is_None(self): self.assertRaises(pyowm.commons.exceptions.ParseAPIResponseError, AirStatus.from_dict, None) From 52f283e9fb31d3cca3e3604ea9ff1ce0f6508b7e Mon Sep 17 00:00:00 2001 From: csparpa Date: Wed, 24 Feb 2021 00:43:45 +0100 Subject: [PATCH 34/36] Air pollution history. Fixes #362 --- .../airpollutionapi30/airpollution_client.py | 14 +++++- .../airpollutionapi30/airpollution_manager.py | 39 +++++++++++++++- pyowm/airpollutionapi30/uris.py | 1 + sphinx/v3/code-recipes.md | 30 +++++++++++++ .../test_integration_pollutionapi30.py | 14 ++++++ .../test_airpollution_manager.py | 45 +++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) diff --git a/pyowm/airpollutionapi30/airpollution_client.py b/pyowm/airpollutionapi30/airpollution_client.py index 71161c82..a92d1855 100644 --- a/pyowm/airpollutionapi30/airpollution_client.py +++ b/pyowm/airpollutionapi30/airpollution_client.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from pyowm.airpollutionapi30.uris import CO_INDEX_URL, OZONE_URL, NO2_INDEX_URL, SO2_INDEX_URL, AIR_POLLUTION_URL, \ - AIR_POLLUTION_FORECAST_URL + AIR_POLLUTION_FORECAST_URL, AIR_POLLUTION_HISTORY_URL from pyowm.utils import formatting @@ -177,6 +177,18 @@ def get_forecast_air_pollution(self, params_dict): _, json_data = self._client.get_json(AIR_POLLUTION_FORECAST_URL, params=params_dict) return json_data + def get_historical_air_pollution(self, params_dict): + """ + Invokes the new AirPollution API history endpoint + + :param params_dict: dict of parameters + :returns: a string containing raw JSON data + :raises: *ValueError*, *APIRequestError* + + """ + _, json_data = self._client.get_json(AIR_POLLUTION_HISTORY_URL, params=params_dict) + return json_data + def __repr__(self): return "<%s.%s - httpclient=%s>" % \ (__name__, self.__class__.__name__, str(self._client)) \ No newline at end of file diff --git a/pyowm/airpollutionapi30/airpollution_manager.py b/pyowm/airpollutionapi30/airpollution_manager.py index 4bce9cc4..c859b17a 100644 --- a/pyowm/airpollutionapi30/airpollution_manager.py +++ b/pyowm/airpollutionapi30/airpollution_manager.py @@ -5,7 +5,7 @@ from pyowm.airpollutionapi30.uris import ROOT_POLLUTION_API_URL, NEW_ROOT_POLLUTION_API_URL from pyowm.commons.http_client import HttpClient from pyowm.constants import AIRPOLLUTION_API_VERSION -from pyowm.utils import geo, decorators +from pyowm.utils import geo, decorators, formatting, timestamps class AirPollutionManager: @@ -239,5 +239,42 @@ def air_quality_forecast_at_coords(self, lat, lon): except: return [] + def air_quality_history_at_coords(self, lat, lon, start, end=None): + """ + Queries the OWM AirPollution API for available forecasted air quality indicators around the specified coordinates. + + :param lat: the location's latitude, must be between -90.0 and 90.0 + :type lat: int/float + :param lon: the location's longitude, must be between -180.0 and 180.0 + :type lon: int/float + :param start: the object conveying the start value of the search time window + :type start: int, ``datetime.datetime`` or ISO8601-formatted string + :param end: the object conveying the end value of the search time window. Values in the future will be clipped + to the current timestamp. Defaults to the current UNIX timestamp. + :type end: int, ``datetime.datetime`` or ISO8601-formatted string + :return: a `list` of *AirStatus* instances or an empty `list` if data is not available + :raises: *ParseResponseException* when OWM AirPollution API responses' data + cannot be parsed, *APICallException* when OWM AirPollution API can not be + reached, *ValueError* for wrong input values + """ + geo.assert_is_lon(lon) + geo.assert_is_lat(lat) + now = timestamps.now(timeformat='unix') + assert start is not None + start = formatting.timeformat(start, 'unix') + if end is None: + end = now + else: + end = formatting.timeformat(end, 'unix') + if end > now: + end = now + + params = {'lon': lon, 'lat': lat, 'start': start, 'end': end} + json_data = self.new_ap_client.get_historical_air_pollution(params) + try: + return airstatus.AirStatus.from_dict(json_data) + except: + return [] + def __repr__(self): return '<%s.%s>' % (__name__, self.__class__.__name__) diff --git a/pyowm/airpollutionapi30/uris.py b/pyowm/airpollutionapi30/uris.py index 5df22d8b..60538b71 100644 --- a/pyowm/airpollutionapi30/uris.py +++ b/pyowm/airpollutionapi30/uris.py @@ -13,3 +13,4 @@ NEW_ROOT_POLLUTION_API_URL = 'openweathermap.org/data/2.5' AIR_POLLUTION_URL = 'air_pollution' AIR_POLLUTION_FORECAST_URL = 'air_pollution/forecast' +AIR_POLLUTION_HISTORY_URL = 'air_pollution/history' diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 2b3a6964..0a4dec25 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -878,6 +878,36 @@ for air_status in list_of_forecasts: air_status.aqi # air quality index ``` +### Getting historical air pollution data on geographic coords +We can get also get historical values for air pollution agents concentration and air quality index: + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.airpollution_manager() + +# fetch history from a certain point in time up to now... +start = 1606223802 # November 24, 2020 +list_of_historical_values = mgr.air_quality_history_at_coords(51.507351, -0.127758, start) # London, GB + +# ...or fetch history on a closed timeframe in the past +end = 1613864065 # February 20, 2021 +list_of_historical_values = mgr.air_quality_history_at_coords(51.507351, -0.127758, start, end=end) # London, GB + +# Each item in the list_of_historical_values is an AirStatus object +for air_status in list_of_historical_values: + air_status.co + air_status.no + air_status.no2 + air_status.o3 + air_status.so2 + air_status.pm2_5 + air_status.pm10 + air_status.nh3 + air_status.aqi # air quality index +``` + +
diff --git a/tests/integration/airpollutionapi30/test_integration_pollutionapi30.py b/tests/integration/airpollutionapi30/test_integration_pollutionapi30.py index 0530b8be..3c499ec6 100644 --- a/tests/integration/airpollutionapi30/test_integration_pollutionapi30.py +++ b/tests/integration/airpollutionapi30/test_integration_pollutionapi30.py @@ -33,6 +33,20 @@ def test_air_quality_forecast_at_coords(self): self.assertIsNotNone(airstatus.reference_time()) self.assertIsNotNone(airstatus.location) + def test_air_quality_history_at_coords(self): + """ + Test feature: get historical air quality data around geo-coordinates. + """ + start = 1606223802 # Tuesday, November 24, 2020 + + list_of_airstatuses = self.__owm.air_quality_history_at_coords(45, 9, start) + self.assertIsInstance(list_of_airstatuses, list) + for airstatus in list_of_airstatuses: + self.assertIsNotNone(airstatus.air_quality_data) + self.assertIsNotNone(airstatus.reception_time()) + self.assertIsNotNone(airstatus.reference_time()) + self.assertIsNotNone(airstatus.location) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/airpollutionapi30/test_airpollution_manager.py b/tests/unit/airpollutionapi30/test_airpollution_manager.py index 2aea30b5..d32c0d10 100644 --- a/tests/unit/airpollutionapi30/test_airpollution_manager.py +++ b/tests/unit/airpollutionapi30/test_airpollution_manager.py @@ -6,6 +6,7 @@ from pyowm.airpollutionapi30 import airpollution_client, airpollution_manager, coindex, so2index, ozone, no2index, airstatus from pyowm.config import DEFAULT_CONFIG from pyowm.constants import AIRPOLLUTION_API_VERSION +from pyowm.utils import timestamps from tests.unit.airpollutionapi30.test_ozone import OZONE_JSON from tests.unit.airpollutionapi30.test_coindex import COINDEX_JSON from tests.unit.airpollutionapi30.test_no2index import NO2INDEX_JSON @@ -32,6 +33,9 @@ def mock_get_air_pollution(self, params_dict): def mock_get_forecast_air_pollution(self, params_dict): return json.loads(AIRSTATUS_MULTIPLE_JSON) + def mock_get_historical_air_pollution(self, params_dict): + return json.loads(AIRSTATUS_MULTIPLE_JSON) + def mock_get_so2_returning_so2index_around_coords(self, params_dict): return json.loads(SO2INDEX_JSON) @@ -229,5 +233,46 @@ def test_air_quality_forecast_at_coords_fails_with_wrong_parameters(self): self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_forecast_at_coords, \ self.__test_instance, 200, 2.5) + def test_air_quality_history_at_coords(self): + ref_to_original = airpollution_client.AirPollutionHttpClient.get_historical_air_pollution + airpollution_client.AirPollutionHttpClient.get_historical_air_pollution = \ + self.mock_get_historical_air_pollution + result = self.__test_instance.air_quality_history_at_coords(45, 9, 12345678) + airpollution_client.AirPollutionHttpClient.get_historical_air_pollution = ref_to_original + self.assertTrue(isinstance(result, list)) + for item in result: + self.assertIsInstance(item, airstatus.AirStatus) + self.assertIsNotNone(item.reference_time) + self.assertIsNotNone(item.reception_time()) + loc = item.location + self.assertIsNotNone(loc) + self.assertIsNotNone(loc.lat) + self.assertIsNotNone(loc.lon) + self.assertIsNotNone(item.air_quality_data) + + def test_air_quality_history_at_coords_fails_with_wrong_parameters(self): + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \ + self.__test_instance, 43.7, -200.0, 12345678, 12349999) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \ + self.__test_instance, 43.7, 200.0, 12345678, 12349999) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \ + self.__test_instance, -200, 2.5, 12345678, 12349999) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \ + self.__test_instance, 200, 2.5, 12345678, 12349999) + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \ + self.__test_instance, 200, 2.5, 'test') + self.assertRaises(ValueError, airpollution_manager.AirPollutionManager.air_quality_history_at_coords, \ + self.__test_instance, 200, 2.5, 'test', 'test2') + + def test_air_quality_history_at_coords_clips_end_param_to_current_timestamp(self): + now = timestamps.now(timeformat='unix') + end = now + 99999999999 + + def assert_clipped(obj, params_dict): + self.assertEqual(params_dict['end'], now) + + airpollution_client.AirPollutionHttpClient.get_historical_air_pollution = assert_clipped + _ = self.__test_instance.air_quality_history_at_coords(45, 9, 12345678, end=end) + def test_repr(self): print(self.__test_instance) From 94d7f16bd94ef0ab8a9529eefbc411b8ec2d3823 Mon Sep 17 00:00:00 2001 From: csparpa Date: Wed, 24 Feb 2021 10:20:22 +0100 Subject: [PATCH 35/36] Dump to 3.2.0 --- pyowm/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyowm/constants.py b/pyowm/constants.py index 75444de7..21deef27 100644 --- a/pyowm/constants.py +++ b/pyowm/constants.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -PYOWM_VERSION = (3, 1, 1) +PYOWM_VERSION = (3, 2, 0) AGRO_API_VERSION = (1, 0, 0) AIRPOLLUTION_API_VERSION = (3, 0, 0) ALERT_API_VERSION = (3, 0, 0) From 5ee75b3a5071bcdec0067233c967e192fbcc3c1f Mon Sep 17 00:00:00 2001 From: csparpa Date: Wed, 24 Feb 2021 10:40:56 +0100 Subject: [PATCH 36/36] updated for release --- Pipfile.lock | 152 +++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index c073a2e1..d6407853 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -124,45 +124,45 @@ }, "cffi": { "hashes": [ - "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", - "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", - "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", - "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", - "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", - "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", - "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", - "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", - "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", - "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", - "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", - "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", - "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", - "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", - "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", - "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", - "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e", - "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", - "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", - "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", - "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", - "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", - "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", - "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", - "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", - "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", - "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", - "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", - "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", - "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", - "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", - "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", - "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", - "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", - "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", - "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", - "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" - ], - "version": "==1.14.4" + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "version": "==1.14.5" }, "chardet": { "hashes": [ @@ -252,23 +252,21 @@ }, "cryptography": { "hashes": [ - "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d", - "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7", - "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901", - "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c", - "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244", - "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6", - "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5", - "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e", - "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c", - "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0", - "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812", - "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a", - "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030", - "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==3.3.1" + "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b", + "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336", + "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87", + "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7", + "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799", + "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b", + "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df", + "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0", + "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3", + "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724", + "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2", + "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.6" }, "distlib": { "hashes": [ @@ -428,11 +426,11 @@ }, "pproxy": { "hashes": [ - "sha256:747aaf8d9ea858cb0a81ec5d6d223abba927879fd8798f7dde54351fc2ee8e49", - "sha256:dfb336a3e794b9a3d06cc95ae94e24f37b28c46765e0ba518015b94d8944782a" + "sha256:e013818b1325603a08769a1c1e36e1ce79482b35069cf64fcdc1c11d34f52fdb", + "sha256:f59e18328f18c8edf601a2b09f857be81f525d043c935e3d5dfa3b31d7f48f1b" ], "index": "pypi", - "version": "==2.5.6" + "version": "==2.7.4" }, "py": { "hashes": [ @@ -452,11 +450,11 @@ }, "pygments": { "hashes": [ - "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", - "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" + "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", + "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" ], "markers": "python_version >= '3.5'", - "version": "==2.7.4" + "version": "==2.8.0" }, "pyparsing": { "hashes": [ @@ -483,10 +481,10 @@ }, "readme-renderer": { "hashes": [ - "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", - "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" + "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", + "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" ], - "version": "==28.0" + "version": "==29.0" }, "recommonmark": { "hashes": [ @@ -523,11 +521,11 @@ }, "secretstorage": { "hashes": [ - "sha256:30cfdef28829dad64d6ea1ed08f8eff6aa115a77068926bcc9f5225d5a3246aa", - "sha256:5c36f6537a523ec5f969ef9fad61c98eb9e017bc601d811e53aa25bece64892f" + "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", + "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" ], "markers": "sys_platform == 'linux'", - "version": "==3.3.0" + "version": "==3.3.1" }, "six": { "hashes": [ @@ -546,11 +544,11 @@ }, "sphinx": { "hashes": [ - "sha256:41cad293f954f7d37f803d97eb184158cfd90f51195131e94875bc07cd08b93c", - "sha256:c314c857e7cd47c856d2c5adff514ac2e6495f8b8e0f886a8a37e9305dfea0d8" + "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9", + "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c" ], "index": "pypi", - "version": "==3.4.3" + "version": "==3.5.1" }, "sphinx-readable-theme": { "hashes": [ @@ -617,11 +615,11 @@ }, "tox": { "hashes": [ - "sha256:65d0e90ceb816638a50d64f4b47b11da767b284c0addda2294cb3cd69bd72425", - "sha256:cf7fef81a3a2434df4d7af2a6d1bf606d2970220addfbe7dea2615bd4bb2c252" + "sha256:89afa9c59c04beb55eda789c7a65feb1a70fde117f85f1bd1c27c66758456e60", + "sha256:ed1e650cf6368bcbc4a071eeeba363c480920e0ed8a9ad1793c7caaa5ad33d49" ], "index": "pypi", - "version": "==3.21.4" + "version": "==3.22.0" }, "tox-travis": { "hashes": [ @@ -633,11 +631,11 @@ }, "tqdm": { "hashes": [ - "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a", - "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65" + "sha256:65185676e9fdf20d154cffd1c5de8e39ef9696ff7e59fe0156b1b08e468736af", + "sha256:70657337ec104eb4f3fb229285358f23f045433f6aea26846cdd55f0fd68945c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.56.0" + "version": "==4.57.0" }, "twine": { "hashes": [