Getting Mars photos from NASA using aiohttp

2017-06-12 python aiohttp nasa

I am a huge fan of the book The Martian by Andy Weir. Reading it I wondered how did Mark Watney feel when he walked around the Red Planet. Recently, thanks to this Twilio blog post I found out that NASA has a public API for accessing photos taken by Mars rovers. However, not being a huge fan of MMS, I decided to write my own application to get the inspiring images delivered straight to my browser.

Creating aiohttp application

Let's start with a simple application, just to get aiohttp up and running. First, create a new virtualenv. It is recommended to use Python 3.5, since we will be using new async def and await syntax. If you want to develop this project further and take advantage of asynchronous comprehensions, you can use Python 3.6 (I did). Next, install aiohttp:

pip install aiohttp

Now create a source file (call it nasa.py ) and put some code inside:

from aiohttp import web


async def get_mars_photo(request):
    return web.Response(text='A photo of Mars')


app = web.Application()
app.router.add_get('/', get_mars_photo, name='mars_photo')

If you are new to aiohttp some things might need explaining:

Note: request handlers don't have to be coroutines, they can be regular functions. But we are going to use the power of asyncio, so most functions in our program are going to be defined with async def.

Running the application

To run your application you can add this line at the end of your file:

web.run_app(app, host='127.0.0.1', port=8080)

And then run it like any other Python script:

python nasa.py

However, there is a better way. Among many third-party libraries you will find aiohttp-devtools. It provides a nice runserver command that detects your app automatically and supports live reloading:

pip install aiohttp-devtools
adev runserver -p 8080 nasa.py

Now, if you visit localhost:8080, you should see A photo of Mars text in your browser.

Using NASA API

Of course, this is not the end. If you are a keen observer, you noticed that we are not getting an actual image, but rather some text. Let's fix that.

To get photos from Mars, we will use NASA API. Each rover has its own URL (for Curiosity it's https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos). We have to provide at least 2 params for each call:

In return we will get a list of photos, each with a URL, camera info and rover manifest.

Modify the nasa.py file to look like this:

import random

from aiohttp import web, ClientSession
from aiohttp.web import HTTPFound

NASA_API_KEY = 'DEMO_KEY'
ROVER_URL = 'https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos'


async def get_mars_image_url_from_nasa():
    while True:
        sol = random.randint(0, 1722)
        params = {'sol': sol, 'api_key': NASA_API_KEY}
        async with ClientSession() as session:
            async with session.get(ROVER_URL, params=params) as resp:
                resp_dict = await resp.json()
        if 'photos' not in resp_dict:
            raise Exception
        photos = resp_dict['photos']
        if not photos:
            continue
        return random.choice(photos)['img_src']


async def get_mars_photo(request):
    url = await get_mars_image_url_from_nasa()
    return HTTPFound(url)

Here is what's going on:

Getting NASA API key

The default DEMO_KEY provided by NASA works fine, but you will soon reach the limit of hourly API calls. I recommend you to get your own API key. You can do it here (the sign up process is very simple and fast).

Now when you run the application, you will be redirected to a pretty image straight from Mars:

A rather uninspiring photo

Well, that's not exactly what I meant ...

Validating an image

The image you just saw is not very inspiring. It turns out that rovers take a lot of really boring photos. I wanted to see what Mark Watney saw on his incredible journey, and this is just not good enough. Let's find a way to fix that.

We will need some sort of validation for our images. Without specifying the criteria yet, we can modify our code:

async def get_mars_photo_bytes():
    while True:
        image_url = await get_mars_image_url_from_nasa()
        async with ClientSession() as session:
            async with session.get(image_url) as resp:
                image_bytes = await resp.read()
        if await validate_image(image_bytes):
            break
    return image_bytes


async def get_mars_photo(request):
    image = await get_mars_photo_bytes()
    return web.Response(body=image, content_type='image/jpeg')

Some new things happened here:

Note: in this code we removed the redirection (HTTPFound), so now we can easily refresh the page to get another image.

Now we need to figure out how to validate the photos. One thing we can do rather easily is to check if the image is big enough. It's not a perfect validation, but it should do for now. To process images, we will need Pillow (PIL fork):

pip install pillow

Our validation function could look like this:

import io
from PIL import Image


async def validate_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))
    return image.width >= 1024 and image.height >= 1024

Mars in shades of gray

Now that's more like it! We can go one step further and reject grayscale images:

async def validate_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))
    return image.width >= 1024 and image.height >= 1024 and image.mode != 'L'

Now our program starts returning much more inspiring photos:

Cool landscape

And, occasionally, a robot selfie:

Rover's selfie

Summary

Our program should now look like this:

import random
import io

from aiohttp import web, ClientSession

from PIL import Image

NASA_API_KEY = 'DEMO_KEY'
ROVER_URL = 'https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos'


async def validate_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))
    return image.width >= 1024 and image.height >= 1024 and image.mode != 'L'


async def get_mars_image_url_from_nasa():
    while True:
        sol = random.randint(0, 1722)
        params = {'sol': sol, 'api_key': NASA_API_KEY}
        async with ClientSession() as session:
            async with session.get(ROVER_URL, params=params) as resp:
                resp_dict = await resp.json()
        if 'photos' not in resp_dict:
            raise Exception
        photos = resp_dict['photos']
        if not photos:
            continue
        return random.choice(photos)['img_src']


async def get_mars_photo_bytes():
    while True:
        image_url = await get_mars_image_url_from_nasa()
        async with ClientSession() as session:
            async with session.get(image_url) as resp:
                image_bytes = await resp.read()
        if await validate_image(image_bytes):
            break
    return image_bytes


async def get_mars_photo(request):
    image = await get_mars_photo_bytes()
    return web.Response(body=image, content_type='image/jpeg')


app = web.Application()
app.router.add_get('/', get_mars_photo, name='mars_photo')

There are many things we could improve (like getting max_sol value from the API, passing the rover's name, caching the URLs) but for now it does the job done: we can get a random, inspiring photo of Mars and feel like we are actually there.

I hope you liked this short tutorial. If you spot a mistake or have any questions, please let me know.