

Discover more from hrbrmstr's Daily Drop
We started off the week talking about the weather, and we'll finish it by building a small CLI to retrieve a forecast — using tomrrow.io — for a given location. Remember: these summer (for us northern hemispherians) WPEs are designed to be super straightforward, so y'all can have some time to go outside and touch grass.
To ensure accessibility for the largest percentage of Drop readers, we'll use bash
for the basic example flow. I think I've commented the example script sufficiently to make what it's doing transferrable to your fav language, but let me know if that is not the case.
NOTE! Since this edition is code-block heavy, there’s a potentially more readable version of it here: https://rud.is/wpe/2023-08-04-drop-wpe.html
The Plan
The basic idea is to take in a location specified as a parameter on the command line and print back the forecast. This means that we need to geocode the input, and for that, we'll turn to geocode.io. They have a generous free tier and have never failed me yet. From their response, we'll pass in the latitude and longitude of the first geocoded result1 to tomorrow.io's forecast endpoint, since they also have a generous free tier.
Now, it doesn't make sense to re-geocode a given address every time we make a call to the CLI (I mean, earthquakes cause plates to shift, but not by that much!). So the actual plan is:
Take a location from the CLI call
See if we've already geocoded it
if so, used a cached response
if not, cache the response and use the one from the API call
Get the lat/lng from the response
Make a call to tomorrow.io to get the forecast
Since we also talked about SQLite this week, we'll use SQLite to cache the geocode.io responses. And, to try to encourage some positive coding idioms, we'll do a bit of helpful/defensive programming along the way.
Getting What We Need From The Caller
We need three things from the caller/calling environment:
GEOCODEIO_API_KEY
(store your geocode.io key in this environment variable)TOMORROWIO_API_KEY
(store your tomorrow.io key in this environment variable)An address to geocode
Here's a fairly defensive/helpful setup for that at the top of the script:
#!/bin/bash
# Get weather forecast for a given location
# Check if any command line input was provided
if [ -z "$*" ]; then
echo "Usage: $(basename "${0}") <address>"
exit 1
fi
# Check if GEOCODEIO_API_KEY and TOMORROWIO_API_KEY are set in the environment
if [ -z "${GEOCODEIO_API_KEY}" ]; then
echo "Error: GEOCODEIO_API_KEY is missing from the environment. Please set the API key for geocode.io."
exit 1
fi
if [ -z "${TOMORROWIO_API_KEY}" ]; then
echo "Error: TOMORROWIO_API_KEY is missing from the environment. Please set the API key for tomorrow.io."
exit 1
fi
NOTE: If you do decide to follow along and extend things in bash
, I highly recommend installing and using shellcheck.
A Place To Call [API Results] Home
Setting up a caching layer means we need to have a caching layer. We'll set that up now:
# Create a new SQLite database file or open an existing one
DB_FILE="cache.db"
sqlite3 "${DB_FILE}" "CREATE TABLE IF NOT EXISTS api_cache (query TEXT PRIMARY KEY, response TEXT);"
TODO (for you):
dropping SQL dbs any old place is bad. Follow XDG user directory best practices.
consider using better file and table names since you may be asked to do more with this SQL database in a forthcoming step.
Show Me The Way To Go Home
The core URL endpoint for geocode.io geocoding looks like this:
https://api.geocod.io/v1.7/geocode?q=${query}&api_key=${GEOCODEIO_API_KEY}
Remember, we want only make the call if the result is not cached already. Let's break this up into functions to make it easier to work the logic later on:
# Function to cache the API response
cache_geocode() {
local query response
query="$1"
response="$2"
sqlite3 "${DB_FILE}" "INSERT OR REPLACE INTO api_cache (query, response) VALUES ('${query}', '${response}');"
}
# Function to get the cached API response
get_cached_geocode() {
local query
query="$1"
sqlite3 "${DB_FILE}" "SELECT response FROM api_cache WHERE query='${query}';"
}
# Function to make the API call
geocode() {
local query response
query=$(urlencode "$1")
response=$(curl -s "https://api.geocod.io/v1.7/geocode?q=${query}&api_key=${GEOCODEIO_API_KEY}")
echo "$response"
}
Given that we have no idea what the caller will input, we need to use that urlencode
function (which we'll see next) to defensively URL encode the parameter to q
. This is a helper function we can use in bash
to do that:
# helper for URL encoding the query string
urlencode() {
local string="$1"
local strlen=${#string}
local encoded=""
for (( pos=0 ; pos<strlen ; pos++ )); do
local c="${string:$pos:1}"
case "${c}" in
[-_.~a-zA-Z0-9] ) encoded+="${c}" ;;
* ) printf -v c '%%%02X' "'${c}"
encoded+="${c}" ;;
esac
done
echo "${encoded}"
}
TODO (for you):
extra code should always be eliminated (if possible). Do we really need this
urlencode
function? Look up the--data-urlencode
parameter forcurl
and see if you can simplify the API calls used in this Drop.if you end up using a “proper” programming language, most have built-in or library-provided URL encoding functions. Do not reinvent the wheel.
Go Home
We now know how to safely make/cache geocod.io API calls/responses. So, let's do that with the command input.
# Get the query string from the command line
QUERY_STRING="$*"
# Check if the response is already cached
CACHED_RESPONSE=$(get_cached_geocode "${QUERY_STRING}")
if [ -z "${CACHED_RESPONSE}" ]; then
# If not cached, make the API call and cache the response
API_RESPONSE=$(geocode "${QUERY_STRING}")
cache_geocode "${QUERY_STRING}" "${API_RESPONSE}"
LOCATION="${API_RESPONSE}"
else
LOCATION="${CACHED_RESPONSE}"
fi
The API response looks something like this:
{
"input": {
"address_components": {
"city": "Berwick",
"state": "ME",
"country": "US"
},
"formatted_address": "Berwick, ME"
},
"results": [
{
"address_components": {
"city": "Berwick",
"county": "York County",
"state": "ME",
"zip": "03901",
"country": "US"
},
"formatted_address": "Berwick, ME 03901",
"location": {
"lat": 43.26855,
"lng": -70.86213
},
"accuracy": 1,
"accuracy_type": "place",
"source": "TIGER/Line® dataset from the US Census Bureau"
}
]
}
We have the location, but what we truly require is the latitude
and longitude
values from the first result. We can lean on jq
for that:
# get the lat/lng pair from it
LAT_LNG="$(urlencode $(echo "${LOCATION}" | jq -rc '.results[0]|"\(.location.lat),\(.location.lng)"'))"
TODO (for you):
ensure you pick the most accurate response, not just the first one
OR, let the caller pick the result they want cached
Pretend You're A Meteorologist
Now, we'll make the call to forecast.io with the data we have:
# Make the forecast call to tomorrow.io
FORECAST="$(curl --silent --request GET \
--url "https://api.tomorrow.io/v4/weather/forecast?location=${LAT_LNG}&units=imperial&apikey={$TOMORROWIO_API_KEY}" \
--header 'accept: application/json')"
PRETTY_ADDRESS="$(echo "${LOCATION}" | jq -rc '.results[0]|"\(.formatted_address)"')"
echo "Forecast for ${PRETTY_ADDRESS} at $(date)"
echo "${FORECAST}" | jq
TODO (for you):
use a similar caching technique to use cached forecast results if another call is made for the same location within an hour
use a better formatted
date
outputlet folks specify a
units
preferencedo something more than print out formatted JSON
FIN
🎗️ Since this edition is code-block heavy, there’s a potentially more readable version of it here: https://rud.is/wpe/2023-08-04-drop-wpe.html
🎗️There is an example script that contains all these code blocks in one file.
You've now got a little weather forecasting app to call your own! If you do end up taking on this WPE challenge, drop a link to your creation in the comments or on 🐘. ☮
depending on how good/bad the input is, geocode.io will return more than one result with an accuracy
score
Drop #310 (2023-08-04): Weekend Project Edition
Hiya. I believe the geocodio links should be to http://geocod.io/, (not http://geocode.io/).