Generating a Daily Pokedex Entry

February 20, 2019

During New Year’s Eve my unofficially-adopted-brother Ethan told me that he was sad because he wasn’t going to have a Pokemon daily calendar in 2019. To me, that sounded like a problem technology could solve. Sure, I couldn’t give him a fancy flip calendar for 2019 but I could send him a text and email every day!

The Plan

The code to send the daily Pokedex entry didn’t need to be ‘production ready’ and to save time and cost I wanted to avoid the complexities of spinning up a cloud server and running a database because the code only needed to run for a few seconds a day. To me it sounded like the perfect job for an AWS Lambda function.

The rough steps I needed to achieve looked something like this:

  • Find a list of all Pokemon and related information
  • Put the Pokemon and their information in a CSV file
  • Shuffle the CSV and give each row a date starting 2019-01-01
  • Set up an AWS Lambda function to run once per day
  • Write the Lambda function to read a Pokemon from the CSV and send a SMS using Twilio and an email using SendGrid

Creating the CSV

Finding a list of Pokemon in a useful structured format proved tricky but luckily after a while I stumbled across PokeAPI, a RESTful JSON API for Pokemon data!

By sending a GET request to https://pokeapi.co/api/v2/pokemon-species?limit=1000 and looking at the count field in the returned JSON I was able to determine that there were 807 Pokemon to assign dates for. A quick check of the final Pokemon (show below) confirmed that the Pokemon ids were sequential (presumably in National Dex order) and that I was fine to move ahead with the next step.

{
      "name": "zeraora",
      "url": "https://pokeapi.co/api/v2/pokemon-species/807/"
}

Although in my initial plan I intended to store Pokemon data in the CSV file, after using the PokeAPI I felt it was easy enough to retrieve data on given a Pokemon’s species id that I could get away with a basic CSV file that looked like the table below. The values of both the id and date column were generating using LibreOffice Calc.

id date
1 2019-01-01
2 2019-01-02
3 2019-01-03

Using this CSV format we can determine the Pokemon for a given date and then grab some relevant facts using PokeAPI. To avoid sending the Pokemon in a predictable order (namely, sequentially by Pokedex number) we shuffle the first column giving us something like this.

id date
251 2019-01-01
25 2019-01-02
52 2019-01-03

Writing the AWS Lambda Handler Function

AWS Lambda and how useful it is (or not)1 is probably the subject of several blog posts on its own. It serves our needs in this situation because it allows a short function to be invoked at a regular interval 2 with a surprisingly generous free tier.

The key things our function needs to do are

  1. Read the CSV and find today’s Pokemon’s id

  2. Query to find information about today’s Pokemon

  3. Build a string of facts to send

  4. Send the SMS and email

Step 1: Read the CSV

Python comes with a CSV package built into its standard library so the only trick in this step is getting today’s date in the correct format.

import datetime
import csv

def get_pokemon_id():
    today = datetime.datetime.today().strftime('%Y-%m-%d')
    try:
        with open('pokedex.csv') as f:
            reader = csv.reader(f)
            for row in reader:
                if row[1] == today:
                    return row[0]
    except:
        print('Error opening pokedex.csv')

In other languages this would be written a little less explicitly but Python, by nature, is very verbose in expressing simple ideas.

Step 2: Query to Find Information About Today’s Pokemon

PokeAPI makes a distinction between a Pokemon and a Pokemon species, to allow a Pokemon to have multiple varieties that belong to the same species. To make this clearer PokeAPI kindly provides an explanation.

A Pokémon Species forms the basis for at least one Pokémon. Attributes of a Pokémon species are shared across all varieties of Pokémon within the species. A good example is Wormadam; Wormadam is the species which can be found in three different varieties, Wormadam-Trash, Wormadam-Sandy and Wormadam-Plant.

To get the full information we need to represent each Pokemon we fetch information for both the species and the specific variety of Pokemon. As each Pokemon species has a ‘default’ variety we first fetch the species and then request the default variety as indicated by the species data.

import requests
import json

BASE = 'https://pokeapi.co/api/v2/'

def get_pokemon_data(pokemon_species):
  # Each species has a default variety that we'll use for height and weight
    default_variety = next((v for v in pokemon_species['varieties'] if v['is_default'] is True), None)
    if default_variety is not None:
        r = requests.get(default_variety['pokemon']['url'])
        return r.json()
    return None

def get_species(id):
    r = requests.get(BASE + 'pokemon-species/' + id)
    return r.json()
  
# This will be our handler
def send():
  id = get_pokemon_id()
    print(f'Id is {id}')
    
    pokemon_species = get_species(id)
    pokemon = get_pokemon_data(pokemon_species)
    name = pokemon_species['name']
    print(f'Pokemon is {name}')

Step 3: Forming a String of Facts

Now we have both the Pokemon’s species and its specific variety we can begin to pull the information into a sentence. Our goal sentence looks something like this: “Arran! Today’s Pokémon is Weavile, the Sharp Claw Pokémon. Weavile is a black ice, and dark Pokemon which stands 1.1m tall and weighs 34kg. They live in cold regions, forming groups of four or five that hunt prey with impressive coordination.” We also want to label legendary Pokemon as being legendary.

import os
import random

LEGENDARY_IDS = [144, 145, 146, 150, 151, 243, 244, 245, 249, 250, 251, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649 ]

def get_text(pokemon, species):
    if pokemon is None or species is None:
        raise Exception('Failed to find information for todays Pokemon id')
    
    id = species['id']
        
    # Get types
    types = ''
    type_count = len(pokemon['types'])
    for i, t in enumerate(pokemon['types']):
        types = types + t['type']['name'] 
        if type_count > 1 and i < type_count - 1:
            types = types + (', ' if i < len(pokemon['types']) - 2 else ', and ' )

    name = (next(x for x in species['names'] if x['language']['name'] == 'en'))['name']
    flavor_texts = (x for x in species['flavor_text_entries'] if x['language']['name'] == 'en')
    flavor_text = random.choice(list(flavor_texts))['flavor_text'].replace('\n',' ').replace('\t', ' ')
    height = pokemon['height'] * 0.1
    weight = pokemon['weight'] * 0.1
    is_legendary = ' legendary ' if int(id) in LEGENDARY_IDS else '' 
    color = species['color']['name']
    genus = (next(x for x in species['genera'] if x['language']['name'] == 'en'))['genus']

    addresse = os.getenv("TO_WHOM")
    text = f'{addresse}! Today\'s Pokémon is {name}, the {genus}. {name} is a {color} {types}{is_legendary} Pokemon which stands {height:.2g}m tall and weighs {weight:.2g}kg. {flavor_text}'
    text = ' '.join(text.split()) # single space it all
    return text

# This will be our handler
def send():
    id = get_pokemon_id()
    print(f'Id is {id}')
    
    pokemon_species = get_species(id)
    pokemon = get_pokemon_data(pokemon_species)
    name = pokemon_species['name']
    print(f'Pokemon is {name}')

    text = get_text(pokemon, pokemon_species)
    print(text)

The first thing we do is construct a sentence-like representation of the Pokemon’s types. Then we select the English representation of the Pokemon’s name and randomly choose a flavour text from the available choices. We also convert the height and weight into kilograms, select the English representation of the Pokemon’s genus, and fetch the name of the recipient from an environment variable.

Then we format the information into one long string, making sure to use sensible precision for the decimal values of height and weight, and strip out any double spaces and new lines.

Step 4: Sending

Sending is actually the easy part of this problem. Twilio and SendGrid provide Python libraries to make the process simple. We just need to add our API keys as environment variables to keep them secret, load them into our program, and then use them to send the text we’ve already produced. We can also store the destination email address and phone number as environment variables to keep them independent from our code.

import os
from twilio.rest import Client

def send_sms(text):
    # Your Account SID from twilio.com/console
    account_sid = os.getenv("TWILIO_SID")
    # Your Auth Token from twilio.com/console
    auth_token  = os.getenv("TWILIO_AUTH_TOKEN")

    client = Client(account_sid, auth_token)

    client.messages.create(
        to=os.getenv("TO_NUMBER"), 
        from_=os.getenv("TWILIO_FROM_NUMBER"),
        body=text)

Sending the SMS is pretty straightforward. To send the email I chose to also send a picture of the Pokemon with the message.

import os
import sendgrid
from sendgrid.helpers.mail import *

def send_email(text, image_url):
    # https://sendgrid.com/docs/ui/account-and-settings/api-keys/
    sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY'))
    from_email = Email(os.environ.get('FROM_EMAIL'))
    to_email = Email(os.environ.get('TO_EMAIL'))
    today_long = datetime.datetime.today().strftime('%A %d %B %Y')
    subject = f"Your Daily Pokemon for {today_long}"
    content = Content("text/plain", text)
    mail = Mail(from_email, subject, to_email, content)
    mail.add_content(Content("text/html", f"<img src=\"{image_url}\"\> <br>" + text))
    sg.client.mail.send.post(request_body=mail.get())

# This is our handler function
def send():
    id = get_pokemon_id()
    print(f'Id is {id}')
    
    pokemon_species = get_species(id)
    pokemon = get_pokemon_data(pokemon_species)
    name = pokemon_species['name']
    print(f'Pokemon is {name}')

    text = get_text(pokemon, pokemon_species)
    print(text)
    
    sprite_url = pokemon['sprites']['front_default'] if random.random() < 0.9 else pokemon['sprites']['front_shiny']
    send_email(text, sprite_url)
    send_sms(text)

The picture that’s sent with the email has 10% chance of being a shiny pokemon, a treat that should occur a little over once per fortnight!

You can find the complete code on Github at arranf/DailyPokedex.

Deploying

As our function depends on the SendGrid and Twilio SDKs as well as requests we have to produce a deployment package, a bit of a pain in the ass. To do this repeatedly I created a script I could use from the root of repository whilst using the virtual environment.

rm function.zip
zip -r9 function.zip ./v-env/lib/python3.6/site-packages
zip -g function.zip function.py pokedex.csv

  1. Typically, not. [return]
  2. Although it actually doesn’t reliably only invoke the function once - it may invoke it multiple times. In this instance, I’m okay with that. In others you need to have a layer of persistence to mark the message as having been sent. i.e. You need a database. [return]
Last Updated: 2019-02-21 15:32