Skip to content

Commit

Permalink
Started #42: three_hours_forecast + daily_forecast = forecast_at_place
Browse files Browse the repository at this point in the history
  • Loading branch information
csparpa committed Nov 13, 2019
1 parent b8c7a57 commit 06a96c5
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 118 deletions.
4 changes: 2 additions & 2 deletions pyowm/weatherapi25/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def __init__(self, reference_time, sunset_time, sunrise_time, clouds, rain,
if heat_index is not None and heat_index < 0:
raise ValueError("'heat index' must be grater than 0")
self.heat_index = heat_index
if utc_offset is not None and utc_offset < 0:
raise ValueError("utc_offset must be greater than 0")
if utc_offset is not None:
assert isinstance(utc_offset, int), "utc_offset must be an integer"
self.utc_offset = utc_offset

def reference_time(self, timeformat='unix'):
Expand Down
72 changes: 26 additions & 46 deletions pyowm/weatherapi25/weather_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,29 +251,45 @@ def weather_around_coords(self, lat, lon, limit=None):
_, json_data = self.http_client.get_json(FIND_OBSERVATIONS_URI, params=params)
return observation.Observation.from_dict_of_lists(json_data)

def three_hours_forecast(self, name):
def forecast_at_place(self, name, interval, limit=None):
"""
Queries the OWM Weather API for three hours weather forecast for the
specified location (eg: "London,uk"). A *Forecaster* object is
returned, containing a *Forecast* instance covering a global streak of
five days: this instance encapsulates *Weather* objects, with a time
interval of three hours one from each other
Queries the OWM Weather API for weather forecast for the
specified location (eg: "London,uk") with the given time granularity.
A *Forecaster* object is returned, containing a *Forecast*: this instance
encapsulates *Weather* objects corresponding to the provided granularity.
:param name: the location's toponym
:type name: str
:param interval: the granularity of the forecast, among `3h` and 'daily'
:type interval: str among `3h` and 'daily'
:param limit: the maximum number of *Weather* items to be retrieved
(default is ``None``, which stands for any number of items)
:type limit: int or ``None``
:returns: a *Forecaster* instance or ``None`` if forecast data is not
available for the specified location
:raises: *ParseResponseException* when OWM Weather API responses' data
cannot be parsed, *APICallException* when OWM Weather API can not be
reached
"""
assert isinstance(name, str), "Value must be a string"
encoded_name = name
params = {'q': encoded_name}
_, json_data = self.http_client.get_json(THREE_HOURS_FORECAST_URI, params=params)
assert isinstance(interval, str), "Interval must be a string"
if limit is not None:
assert isinstance(limit, int), "'limit' must be an int or None"
if limit < 1:
raise ValueError("'limit' must be None or greater than zero")
params = {'q': name}
if limit is not None:
params['cnt'] = limit
if interval == '3h':
uri = THREE_HOURS_FORECAST_URI
elif interval == 'daily':
uri = DAILY_FORECAST_URI
else:
raise ValueError("Unsupported time interval for forecast")
_, json_data = self.http_client.get_json(uri, params=params)
fc = forecast.Forecast.from_dict(json_data)
if fc is not None:
fc.interval = "3h"
fc.interval = interval
return forecaster.Forecaster(fc)
else:
return None
Expand Down Expand Up @@ -336,42 +352,6 @@ def three_hours_forecast_at_id(self, id):
else:
return None

def daily_forecast(self, name, limit=None):
"""
Queries the OWM Weather API for daily weather forecast for the specified
location (eg: "London,uk"). A *Forecaster* object is returned,
containing a *Forecast* instance covering a global streak of fourteen
days by default: this instance encapsulates *Weather* objects, with a
time interval of one day one from each other
:param name: the location's toponym
:type name: str
:param limit: the maximum number of daily *Weather* items to be
retrieved (default is ``None``, which stands for any number of
items)
:type limit: int or ``None``
:returns: a *Forecaster* instance or ``None`` if forecast data is not
available for the specified location
:raises: *ParseResponseException* when OWM Weather API responses' data
cannot be parsed, *APICallException* when OWM Weather API can not be
reached, *ValueError* if negative values are supplied for limit
"""
assert isinstance(name, str), "Value must be a string"
if limit is not None:
assert isinstance(limit, int), "'limit' must be an int or None"
if limit < 1:
raise ValueError("'limit' must be None or greater than zero")
params = {'q': name}
if limit is not None:
params['cnt'] = limit
_, json_data = self.http_client.get_json(DAILY_FORECAST_URI, params=params)
fc = forecast.Forecast.from_dict(json_data)
if fc is not None:
fc.interval= "daily"
return forecaster.Forecaster(fc)
else:
return None

def daily_forecast_at_coords(self, lat, lon, limit=None):
"""
Queries the OWM Weather API for daily weather forecast for the specified
Expand Down
8 changes: 4 additions & 4 deletions sphinx/usage-examples-v2/weather-api-object-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ The _OWM25_ class extends the _OWM_ abstract base class and provides a method fo

# WEATHER FORECAST QUERYING
* find 3 hours weather forecast at a specific
location --------------------------------------> eg: owm.three_hours_forecast('Venice,IT')
location --------------------------------------> eg: owm.forecast_at_place('Venice,IT', '3h')
* find daily weather forecast at a specific
location --------------------------------------> eg: owm.daily_forecast('San Francisco,US')
location --------------------------------------> eg: owm.forecast_at_place('San Francisco,US', 'daily')

# WEATHER HISTORY QUERYING
* find weather history for a specific location --> eg: owm.weather_history_at_place('Kiev,UA')
Expand Down Expand Up @@ -136,8 +136,8 @@ from the forecast, you can do:
### The Forecaster class
Instances of this class are returned by weather forecast queries such as:

f = owm.three_hours_forecast('London')
f = owm.daily_forecast('Buenos Aires',limit=6)
f = owm.forecast_at_place('London,GB', '3h')
f = owm.forecast_at_place('Buenos Aires,AR', 'daily', limit=6)

A _Forecaster_ object wraps a _Forecast_ object and provides convenience methods that makes it possible to perform complex weather forecast data queries, which could not otherwise be possible using only the _Forecast_ class interface. A central concept with this regard is the "time coverage" of the forecast, that is to say the temporal length of the forecast.

Expand Down
2 changes: 1 addition & 1 deletion sphinx/usage-examples-v2/weather-api-usage-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ The 3h forecasts are provided for a streak of 5 days since the request time and
You can query for 3h forecasts for a location using:

# Query for 3 hours weather forecast for the next 5 days over London
>>> fc = owm.three_hours_forecast('London,uk')
>>> fc = owm.forecast_at_place('London,GB', '3h')

You can query for daily forecasts using:

Expand Down
43 changes: 0 additions & 43 deletions tests/integration/weatherapi25/external_configuration.py

This file was deleted.

14 changes: 7 additions & 7 deletions tests/integration/weatherapi25/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_weather_at_place(self):
"""
Test feature: get currently observed weather at specific location
"""
o1 = self.__owm.weather_at_place('London,uk')
o1 = self.__owm.weather_at_place('London,GB')
o2 = self.__owm.weather_at_place('Kiev')
self.assertTrue(o1 is not None)
self.assertTrue(o1.reception_time() is not None)
Expand Down Expand Up @@ -182,12 +182,12 @@ def test_weather_around_coords(self):
weat = item.weather
self.assertTrue(weat is not None)

def test_three_hours_forecast(self):
def test_forecast_at_place_on_3h(self):
"""
Test feature: get 3 hours forecast for a specific location
"""
fc1 = self.__owm.three_hours_forecast("London,uk")
fc2 = self.__owm.three_hours_forecast('Kiev')
fc1 = self.__owm.forecast_at_place("London,GB", "3h")
fc2 = self.__owm.forecast_at_place('Kiev', "3h")
self.assertTrue(fc1)
f1 = fc1.forecast
self.assertTrue(f1 is not None)
Expand Down Expand Up @@ -269,12 +269,12 @@ def test_three_hours_forecast_at_id(self):
except api_response_error.NotFoundError:
pass # ok

def test_daily_forecast(self):
def forecast_at_place_daily(self):
"""
Test feature: get daily forecast for a specific location
"""
fc1 = self.__owm.daily_forecast("London,uk")
fc2 = self.__owm.daily_forecast('Kiev')
fc1 = self.__owm.forecast_at_place("London,GB", "daily")
fc2 = self.__owm.forecast_at_place('Kiev', "daily")
self.assertTrue(fc1)
f1 = fc1.forecast
f1 = fc1.forecast
Expand Down
14 changes: 13 additions & 1 deletion tests/unit/weatherapi25/test_weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class TestWeather(unittest.TestCase):
'"weather_icon_name": "04d", "humidity": 57, "wind": ' \
'{"speed": 1.1, "deg": 252.002, "gust": 2.09}, "utc_offset": null}'

def test_init_fails_when_negative_data_provided(self):
def test_init_fails_when_wrong_data_provided(self):
self.assertRaises(ValueError, Weather, -9876543210,
self.__test_sunset_time, self.__test_sunrise_time, self.__test_clouds,
self.__test_rain, self.__test_snow, self.__test_wind,
Expand Down Expand Up @@ -169,6 +169,18 @@ def test_init_stores_negative_sunrise_time_as_none(self):
self.__test_humidex, self.__test_heat_index)
self.assertIsNone(instance.sunrise_time())

def test_init_fails_with_non_integer_utc_offset(self):
self.assertRaises(AssertionError, 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,
'non_string_utc_offset')

def test_from_dict(self):
dict1 = {'clouds': {'all': 92}, 'name': 'London',
'coord': {'lat': 51.50853, 'lon': -0.12574},
Expand Down
32 changes: 18 additions & 14 deletions tests/unit/weatherapi25/test_weather_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,21 @@ def test_weather_around_coords_fails_when_coordinates_out_of_bounds(self):
def test_weather_around_coords_fails_with_wrong_params(self):
self.assertRaises(ValueError, WeatherManager.weather_around_coords, self.__test_instance, 43.7, 20.0, -3)

def test_three_hours_forecast(self):
def test_forecast_at_place_fails_with_wrong_params(self):
self.assertRaises(AssertionError, WeatherManager.forecast_at_place,
self.__test_instance, None, "daily", 3)
self.assertRaises(AssertionError, WeatherManager.forecast_at_place,
self.__test_instance, "London,uk", None, -3)
self.assertRaises(ValueError, WeatherManager.forecast_at_place,
self.__test_instance, "London,uk", "wrong", 3)
self.assertRaises(ValueError, WeatherManager.forecast_at_place,
self.__test_instance, "London,uk", "daily", -3)

def test_forecast_at_place_on_3h(self):
original_func = HttpClient.get_json
HttpClient.get_json = \
self.mock_api_call_returning_3h_forecast
result = self.__test_instance.three_hours_forecast("London,uk")
result = self.__test_instance.forecast_at_place("London,uk", "3h")
HttpClient.get_json = original_func
self.assertTrue(isinstance(result, Forecaster))
forecast = result.forecast
Expand All @@ -254,11 +264,11 @@ def test_three_hours_forecast(self):
for weather in forecast:
self.assertTrue(isinstance(weather, Weather))

def test_three_hours_forecast_when_forecast_not_found(self):
def test_forecast_at_place_on_3h_when_forecast_not_found(self):
original_func = HttpClient.get_json
HttpClient.get_json = \
self.mock_api_call_returning_empty_3h_forecast
result = self.__test_instance.three_hours_forecast("London,uk")
result = self.__test_instance.forecast_at_place("London,uk", "3h")
HttpClient.get_json = original_func
self.assertIsNone(result)

Expand Down Expand Up @@ -326,11 +336,11 @@ def test_three_hours_forecast_at_id_fails_with_wrong_params(self):
self.assertRaises(ValueError, WeatherManager.three_hours_forecast_at_id,
self.__test_instance, -1234)

def test_daily_forecast(self):
def test_forecast_at_place_daily(self):
original_func = HttpClient.get_json
HttpClient.get_json = \
self.mock_api_call_returning_daily_forecast
result = self.__test_instance.daily_forecast("London,uk", 2)
result = self.__test_instance.forecast_at_place("London,uk", "daily", 2)
HttpClient.get_json = original_func
self.assertTrue(isinstance(result, Forecaster))
forecast = result.forecast
Expand All @@ -342,17 +352,11 @@ def test_daily_forecast(self):
for weather in forecast:
self.assertTrue(isinstance(weather, Weather))

def test_daily_forecast_fails_with_wrong_params(self):
self.assertRaises(AssertionError, WeatherManager.daily_forecast,
self.__test_instance, 2, 3)
self.assertRaises(ValueError, WeatherManager.daily_forecast,
self.__test_instance, "London,uk", -3)

def test_daily_forecast_when_forecast_not_found(self):
def test_forecast_at_place_daily_when_forecast_not_found(self):
original_func = HttpClient.get_json
HttpClient.get_json = \
self.mock_api_call_returning_empty_daily_forecast
result = self.__test_instance.daily_forecast('London,uk')
result = self.__test_instance.forecast_at_place('London,uk', "daily")
HttpClient.get_json = original_func
self.assertIsNone(result)

Expand Down

0 comments on commit 06a96c5

Please sign in to comment.