We have talked different topics in CodeAStar here, ranging from AWS, CNN, Docker, LGB, Raspberry Pi, RNN and the list goes on. Do you know what is the most popular page here? According to the figure from Google Analytics, and out of my expectation, the most popular page is “Easy Weather Forecast Tool in Python“. In that page, we talked about using Dark Sky API to make an easy Python program for weather forecast. So this time, let’s do another weather forecast again. Our weather API to use, will be the high profiling and one of the most accurate weather forecasts — AccuWeather.
Forecast the Weather Forecast Program
Before we start making our weather forecast program, we should forecast what will our program look like. First things first, let’s go to AccuWeather APIs website to get a free developer account. After that, we can take a look on their API reference to see what outputs they can provide.
The AccuWeather API allows current date and its designed N-day weather forecast submissions. Unlike the Dark Sky API, which we can submit any date range for weather forecast. We also find that the AccuWeather API use an unique location key as input. And the location key is assigned from AccuWeather rather than using longitude and latitude values. Then another important fact we need to know is — we have 50 calls a day for a free account.
Let’ sum up our findings so far:
- the API only allows certain day ranges as inputs
- each forecast call needs a location key id
- we have 50 free API calls per day
And we expect our work flow would be:
It seems that we need at least 2 API calls (one for location key and one for weather information), to make a weather forecast. And we have only 50 free calls per day… Is there anyway to reduce the number of API calls?
A Tiny DB
Yes, we have a solution. Once we got a location key, we store it in our side. So we don’t need to call AccuWeather again for the same location. Then what we need is a database (DB) to store and search the keys. But isn’t installing a DB for our tiny weather program a little bit over? Yes, if you are talking about DBs like MySQL, MongoDB, Microsoft SQL Server….. However, we only need a tiny DB to run search, save and load functions. Then it is not an issue at all. As we have TinyDB.
TinyDB is a NoSQL document based DB written in Python. You can think about using a MongoDB which is small and no installation needed. We just need to include it in our package then import it to our code to use it. Now the problem is solved, let’s get ready to rumble!
AccuWeather Forecast Development
First, we set up our environment using the good o’ pipenv tool:
$pipenv --three
$pipenv shell
$pipenv install requests
$pipenv install tinydb
Create a “accuw_forecast.py” file and import following libraries:
from tinydb import TinyDB, Query
import requests
import json, re, sys, argparse, os
from datetime import datetime
Then we create a database, “accuw_db.json” for our program. Yes, the db is actually a JSON file. We have 2 tables here, “Location” is a table storing location keys. Another table, “Profile“, is a table storing our API key and metric unit.
db = TinyDB('accuw_db.json')
Location = db.table('Location')
Profile = db.table('Profile')
The program will try to load the AccuWeather API key from environment variable “ACW_API_KEY“. Then it will update the API key in our Profile table.
if "ACW_API_KEY" in os.environ:
API_KEY = os.environ['ACW_API_KEY']
Profile.upsert({'api_key':API_KEY}, Query().api_key.exists())
else:
API_KEY = Profile.search(Query().api_key)
if API_KEY == []:
sys.exit("No API key found")
API_KEY = API_KEY[0]['api_key']
For location and metric unit, we can enter them from command line arguments. The program will store them to Profile table. Then we don’t need to enter them again, unless you want to change to other location/metric unit.
def is_input_inputted(input_var, table, field_name):
if input_var is None:
input_var = table.search(Query()[field_name])
if input_var == []:
parser.print_help()
sys.exit()
input_var = input_var[0][field_name]
else:
table.upsert({field_name:input_var}, Query()[field_name].exists())
return input_var
parser = argparse.ArgumentParser(description='AccuWeather Forecast for Python')
parser.add_argument('-l', action="store", dest="location", help='location for weather forecast, e.g. "Tokyo"')
parser.add_argument('-m', action="store", dest="metric", choices=['c', 'f'], help='metric for weather forecast, c or f', default="c", type=str.lower )
args = parser.parse_args()
location = is_input_inputted(args.location,Profile, "last_location")
metric = is_input_inputted(args.metric, Profile, "last_metric")
How location works?
We have an idea to store location key for each weather forecast call. It can reduce the number of calls if we forecast the same single location. But what if we try to forecast different locations? It still requires many API calls. Don’t panic, we have another idea.
When we run our AccuWeather program (ACW) for the first time, we can make an API call to get the 150 most popular locations from AccuWeather.com. We then capture their names, administrative areas and countries. So no more extra call is needed, if we try to forecast those locations. Please note that administrative areas are used to identify the same locations in different places. You can think about the case of “Springfield, New Jersey” and “Springfield, Illinois” and “Springfield, The Simpsons”.
def getJSONfromUrl(url):
response = requests.get(url)
json_data = json.loads(response.text)
return json_data
if (Location.count(Query()) == 0):
url = f"http://dataservice.accuweather.com/locations/v1/topcities/150?apikey={API_KEY}"
json_data = getJSONfromUrl(url)
for p in json_data:
Location.insert({'name': p['LocalizedName'], 'key': p['Key'], 'english_name':p['EnglishName'],
'administrative_area':p['AdministrativeArea']['ID'], 'country': p['Country']['EnglishName']})
After that, the ACW will first search the location from our local DB, then it will return the location key. If no key is found, it will make an API call to get the location key then store it in our DB.
location_from_db = Location.search(Query().name.matches(location, flags=re.IGNORECASE))
if location_from_db == []:
url = f"http://dataservice.accuweather.com/locations/v1/search?apikey={API_KEY}&q={location}"
json_data = getJSONfromUrl(url)
if json_data == []:
sys.exit(f"No location found for '{location}' from AccuWeather API")
else:
for p in json_data:
Location.insert({'name': location, 'key': p['Key'], 'english_name':p['EnglishName'],
'administrative_area':p['AdministrativeArea']['ID'],'country': p['Country']['EnglishName']})
break
location_from_db = Location.search(Location.name.matches(location, flags=re.IGNORECASE))
location_key = location_from_db[0]['key']
admin_area = location_from_db[0]['administrative_area']
country = location_from_db[0]['country']
Get the Weather
Now we have the location key, we can use it to call the weather forecast APIs. Since there are different forecast APIs, ranging from current date, 1-hour, 1-day, 72-hour, 5-day, 10-day…. We pick the APIs that suit us best. Of course we need to know the current weather info, so we pick the current date API. And for the next n-day forecast, I think 5-day is good enough for accuracy and effectiveness.
The next thing we need to do is finding which attributes to display in our ACW. Again, there are many weather attributes as well. We pick maximum and minimum temperatures, weather description, raining possibility and wind speed as our major info to show in the ACW. You can add or remove info later. Please note that, wind speed is selected because of the wind chill effect. TL;DR, faster wind speed blows out warm air around our skin. We then use more energy to cover the heat loss. Thus windier makes us feel cooler. Okay, let’s focus back on our code, first we print out the current weather info:
def getFormattedDateTime(datestr):
p_datestr_format = ''.join(datestr.rsplit(":", 1))
date_object = datetime.strptime(p_datestr_format, '%Y-%m-%dT%H:%M:%S%z')
return date_object.strftime("%H:%M %A, %d %B %Y Timezone:%z")
url = f"http://dataservice.accuweather.com/currentconditions/v1/{location_key}?apikey={API_KEY}&details=true"
json_data = getJSONfromUrl(url)
unit = "Metric" if (metric == "c") else "Imperial"
metric_tag = "true" if (metric == "c") else "false"
for p in json_data:
current_weather=p["WeatherText"]
current_temp=p["Temperature"][unit]
wind_speed=p["Wind"]["Speed"][unit]
date_w_format = getFormattedDateTime(p["LocalObservationDateTime"])
char_length = 50
print(f"Location: {location}, {admin_area}, {country}")
print(f"Local observation time: {date_w_format}")
print(f"Current weather status: {current_weather}")
print(f"Current temperature: {current_temp['Value']} {current_temp['Unit']}")
print(f"Wind speed: {wind_speed['Value']} {wind_speed['Unit']}")
print(f"\n{'='*char_length}")
When we use “Los Angeles” as forecast location,
$export ACW_API_KEY=abcde
$python accuw_forecast.py -l "Los Angeles" -m c
It should print out:
Location: Los Angeles, CA, United States
Local observation time: 02:23 Friday, 31 May 2019 Timezone:-0700
Current weather status: Cloudy
Current temperature: 15.6 C
Wind speed: 5.6 km/h
It looks good and the 5-day forecast part is similar to the the current weather part:
url = f"http://dataservice.accuweather.com/forecasts/v1/daily/5day/{location_key}?apikey={API_KEY}&details=true&metric={metric_tag}"
json_data = getJSONfromUrl(url)
print(f"5-day summery: {json_data['Headline']['Text']}")
for d in json_data["DailyForecasts"]:
print(f"{'-'*char_length}")
print(f"Date: {getFormattedDateTime( d['Date'])}")
print(f"Min temperature: {d['Temperature']['Minimum']['Value']} {d['Temperature']['Minimum']['Unit']}")
print(f"Max temperature: {d['Temperature']['Maximum']['Value']} {d['Temperature']['Maximum']['Unit']}")
print(f"Description: {d['Day']['LongPhrase']}")
print(f"Rain probability: {d['Day']['RainProbability']} %")
print(f"Wind speed: {d['Day']['Wind']['Speed']['Value']} {d['Day']['Wind']['Speed']['Unit']}")
And our output should look like:
==================================================
5-day summery: Mostly cloudy this weekend
--------------------------------------------------
Date: 07:00 Friday, 31 May 2019 Timezone:-0700
Min temperature: 13.9 C
Max temperature: 22.2 C
Description: Low clouds giving way to sunshine
Rain probability: 4 %
Wind speed: 9.7 km/h
--------------------------------------------------
Date: 07:00 Saturday, 01 June 2019 Timezone:-0700
Min temperature: 13.9 C
Max temperature: 21.1 C
Description: Low clouds followed by some sun
Rain probability: 10 %
Wind speed: 9.7 km/h
......
That’s it! We’ve just finished another weather forecast program. Then we go for our next topic— Quality Assurance.
Quality Assurance on AccuWeather program
We believe everyone codes in the future, no matter it is coded by human or A.I.. In order to code better than others, we run quality assurance to make good our products. Then it comes up another question, what is quality assurance?
Quality assurance (QA) is a set of processes to ensure our products meet requirements. It is including but not limited to testing. It can be applied to the planning stage of the Software Development Life Cycle (SDLC). Do you remember we think about getting bulk locations from the first ACW run? And the case to avoid forecasting locations with the same name?
Yes, those are processes of QA to ensure the program is robust. We also include wind speed in the ACW to let users consider the wind chill factor. We should document those QA processes, so we can apply the pattern for similar projects.
Since we are running QA, why don’t we test our program once again? This time, just run the program without arguments, to test the Profile functionality.
$python accuw_forecast.py
Then it should print out:
Location: Los Angeles, CA, United States Local observation time: 10:53 Friday, 31 May 2019 Timezone:-0 Current weather status: Cloudy Current temperature: 18.3 C Wind speed: 0.0 km/h ================================================== 5-day summery: Mostly cloudy this weekend -------------------------------------------------- Date: 07:00 Friday, 31 May 2019 Timezone:-0700 Min temperature: 14.4 C Max temperature: 22.8 C Description: Low clouds giving way to sunshine Rain probability: 4 % Wind speed: 6.4 km/h -------------------------------------------------- Date: 07:00 Saturday, 01 June 2019 Timezone:-0700 Min temperature: 13.9 C Max temperature: 21.1 C Description: Low clouds followed by some sun Rain probability: 10 % Wind speed: 9.7 km/h -------------------------------------------------- Date: 07:00 Sunday, 02 June 2019 Timezone:-0700 Min temperature: 14.4 C Max temperature: 21.1 C Description: Low clouds followed by some sun Rain probability: 12 % Wind speed: 9.7 km/h -------------------------------------------------- Date: 07:00 Monday, 03 June 2019 Timezone:-0700 Min temperature: 15.0 C Max temperature: 22.6 C Description: Low clouds followed by sunshine Rain probability: 3 % Wind speed: 9.3 km/h -------------------------------------------------- Date: 07:00 Tuesday, 04 June 2019 Timezone:-0700 Min temperature: 15.6 C Max temperature: 23.6 C Description: Low clouds followed by sunshine Rain probability: 0 % Wind speed: 9.3 km/h
What have we learnt in this post?
- Development of weather forecast program using AccuWeather API
- Usage of TinyDB
- Apply quality assurance to make good our products
(the complete source can be found at GitHub: https://github.com/codeastar/ez_accuweather_python )
Pingback: Save and Load your RNN model ⋆ Code A Star Learn Machine Learning
Pingback: PWA - Create Easy Progressive Web App with React ⋆ Code A Star