Drop #310 (2023-08-04): Weekend Project Edition

Don’t Put Off Today

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

Jessica Gilmore on Twitter: "In the words of Hannibal, “I love it when a  plan comes together”. — #ATeam #PlanForTheBest #Goals #Hannibal  https://t.co/bSAzSY0gAc" / Twitter

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

selective focus photo of person laying hand over the sunset

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

brown wooden box

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

a person pointing at a map with pins on it

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 for curl 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

white house under maple trees

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 output

  • let folks specify a units preference

  • do 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 🐘. ☮

1

depending on how good/bad the input is, geocode.io will return more than one result with an accuracy score

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.