Creating the application's model
Let's start creating the model that will represent all the information that our application will scrape from the weather website. The first item we are going to add is an enumeration to represent each option of the weather forecast we will provide to the users of our application. Create a file named forecast_type.py in the directory weatherterm/core with the following contents:
from enum import Enum, unique
@unique
class ForecastType(Enum):
TODAY = 'today'
FIVEDAYS = '5day'
TENDAYS = '10day'
WEEKEND = 'weekend'
Enumerations have been in Python's standard library since version 3.4 and they can be created using the syntax for creating classes. Just create a class inheriting from enum.Enum containing a set of unique properties set to constant values. Here, we have values for the four types of forecast that the application will provide, and where values such as ForecastType.TODAY, ForecastType.WEEKEND, and so on can be accessed.
Note that we are assigning constant values that are different from the property item of the enumeration, the reason being that later these values will be used to build the URL to make requests to the weather website.
The application needs one more enumeration to represent the temperature units that the user will be able to choose from in the command line. This enumeration will contain Celsius and Fahrenheit items.
First, let's include a base enumeration. Create a file called base_enum.py in the weatherterm/core directory with the following contents:
from enum import Enum
class BaseEnum(Enum):
def _generate_next_value_(name, start, count, last_value):
return name
BaseEnum is a very simple class inheriting from Enum . The only thing we want to do here is override the method _generate_next_value_ so that every enumeration that inherits from BaseEnum and has properties with the value set to auto() will automatically get the same value as the property name.
Now, we can create an enumeration for the temperature units. Create a file called unit.py in the weatherterm/core directory with the following content:
from enum import auto, unique
from .base_enum import BaseEnum
@unique
class Unit(BaseEnum):
CELSIUS = auto()
FAHRENHEIT = auto()
This class inherits from the BaseEnum that we just created, and every property is set to auto(), meaning the value for every item in the enumeration will be set automatically for us. Since the Unit class inherits from BaseEnum, every time the auto() is called, the _generate_next_value_ method on BaseEnum will be invoked and will return the name of the property itself.
Before we try this out, let's create a file called __init__.py in the weatherterm/core directory and import the enumeration that we just created, like so:
from .unit import Unit
If we load this class in the Python REPL and check the values, the following will occur:
Python 3.6.2 (default, Sep 11 2017, 22:31:28)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from weatherterm.core import Unit
>>> [value for key, value in Unit.__members__.items()]
[<Unit.CELSIUS: 'CELSIUS'>, <Unit.FAHRENHEIT: 'FAHRENHEIT'>]
Another item that we also want to add to the core module of our application is a class to represent the weather forecast data that the parser returns. Let's go ahead and create a file named forecast.py in the weatherterm/core directory with the following contents:
from datetime import date
from .forecast_type import ForecastType
class Forecast:
def __init__(
self,
current_temp,
humidity,
wind,
high_temp=None,
low_temp=None,
description='',
forecast_date=None,
forecast_type=ForecastType.TODAY):
self._current_temp = current_temp
self._high_temp = high_temp
self._low_temp = low_temp
self._humidity = humidity
self._wind = wind
self._description = description
self._forecast_type = forecast_type
if forecast_date is None:
self.forecast_date = date.today()
else:
self._forecast_date = forecast_date
@property
def forecast_date(self):
return self._forecast_date
@forecast_date.setter
def forecast_date(self, forecast_date):
self._forecast_date = forecast_date.strftime("%a %b %d")
@property
def current_temp(self):
return self._current_temp
@property
def humidity(self):
return self._humidity
@property
def wind(self):
return self._wind
@property
def description(self):
return self._description
def __str__(self):
temperature = None
offset = ' ' * 4
if self._forecast_type == ForecastType.TODAY:
temperature = (f'{offset}{self._current_temp}\xb0\n'
f'{offset}High {self._high_temp}\xb0 / '
f'Low {self._low_temp}\xb0 ')
else:
temperature = (f'{offset}High {self._high_temp}\xb0 / '
f'Low {self._low_temp}\xb0 ')
return(f'>> {self.forecast_date}\n'
f'{temperature}'
f'({self._description})\n'
f'{offset}Wind: '
f'{self._wind} / Humidity: {self._humidity}\n')
In the Forecast class, we will define properties for all the data we are going to parse:
We can also implement two methods called forecast_date with the decorators @property and @forecast_date.setter . The @property decorator will turn the method into a getter for the _forecast_date property of the Forecast class, and the @forecast_date.setter will turn the method into a setter. The setter was defined here because, every time we need to set the date in an instance of Forecast, we need to make sure that it will be formatted accordingly. In the setter, we call the strftime method, passing the format codes %a (weekday abbreviated name), %b (monthly abbreviated name), and %d (day of the month).
Lastly, we override the __str__ method to allow us to format the output the way we would like when using the print, format, and str functions.
By default, the temperature unit used by weather.com is Fahrenheit, and we want to give the users of our application the option to use Celsius instead. So, let's go ahead and create one more file in the weatherterm/core directory called unit_converter.py with the following content:
from .unit import Unit
class UnitConverter:
def __init__(self, parser_default_unit, dest_unit=None):
self._parser_default_unit = parser_default_unit
self.dest_unit = dest_unit
self._convert_functions = {
Unit.CELSIUS: self._to_celsius,
Unit.FAHRENHEIT: self._to_fahrenheit,
}
@property
def dest_unit(self):
return self._dest_unit
@dest_unit.setter
def dest_unit(self, dest_unit):
self._dest_unit = dest_unit
def convert(self, temp):
try:
temperature = float(temp)
except ValueError:
return 0
if (self.dest_unit == self._parser_default_unit or
self.dest_unit is None):
return self._format_results(temperature)
func = self._convert_functions[self.dest_unit]
result = func(temperature)
return self._format_results(result)
def _format_results(self, value):
return int(value) if value.is_integer() else f'{value:.1f}'
def _to_celsius(self, fahrenheit_temp):
result = (fahrenheit_temp - 32) * 5/9
return result
def _to_fahrenheit(self, celsius_temp):
result = (celsius_temp * 9/5) + 32
return result
This is the class that is going to make the temperature conversions from Celsius to Fahrenheit and vice versa. The initializer of this class gets two arguments; the default unit used by the parser and the destination unit. In the initializer, we will define a dictionary containing the functions that will be used for temperature unit conversion.
The convert method only gets one argument, the temperature. Here, the temperature is a string, so the first thing we need to do is try converting it to a float value; if it fails, it will return a zero value right away.
You can also verify whether the destination unit is the same as the parser's default unit or not. In that case, we don't need to continue and perform any conversion; we simply format the value and return it.
If we need to perform a conversion, we can look up the _convert_functions dictionary to find the conversion function that we need to run. If we find the function we are looking for, we invoke it and return the formatted value.
The code snippet below shows the _format_results method, which is a utility method that will format the temperature value for us:
return int(value) if value.is_integer() else f'{value:.1f}'
The _format_results method checks if the number is an integer; the value.is_integer() will return True if the number is, for example, 10.0. If True, we will use the int function to convert the value to 10; otherwise, the value is returned as a fixed-point number with a precision of 1. The default precision in Python is 6. Lastly, there are two utility methods that perform the temperature conversions, _to_celsius and _to_fahrenheit.
Now, we only need to edit the __init__.py file in the weatherterm/core directory and include the following import statements:
from .base_enum import BaseEnum
from .unit_converter import UnitConverter
from .forecast_type import ForecastType
from .forecast import Forecast