A toy geolocation API in Python

I spent some time last night thinking about ways in which I could improve the Awesome Blogdown website. One of the things that I was interested in was the country where each site is located and I decided it would be fun to create a little web API so I could do country lookups based on either an IP address or a domain name.

I’ve done some geolocation stuff in the past, but only using Linux command line tools and I did briefly consider building out an API on top of them but thankfully I resisted. As anyone that knows me is probably aware by now my go-to tool for creating APIs is Jeff Allen’s plumber package for R, but I haven’t written much Python lately, so I thought I’d give that a go instead.

The API code

My favourite tool for writing API’s in Python is Flask, which I believe served as part of the inspiration for plumber. It’s small and fast, but I’ve found it to be endlessly flexible when used for larger projects.

In this toy example I’m also using the json library, for outputs from the API and the GeoIP library for our IP/domain name to country conversions. There is a GeoIP2, also from Maxmind, but it seemed a bit more complicated to use, and I was only experimenting, so I went with the older version.

I’m using ubuntu Linux on my home server, along with Python 3, so I install packages on the command line as follows:

pip3 install PACKAGENAME

Once the required packages are installed I wrote out my little API as like this:

#!/usr/bin/env python3
"""Small command line script to start a toy geolocation API example"""
import json
import GeoIP
from flask import Flask
from flask import Response
app = Flask(__name__)

@app.route("/")
def api_info():
    """Returns a helpful message about usage"""
    msg = ("Geolocate by IP or domain name"
           "<pre>/country_by_ip/&lt;ip_addr&gt;"
           "\n/country_by_name/&lt;domain_name&gt;</pre>")
    return msg

@app.route("/country_by_ip/<ip_addr>")
def country_by_ip(ip_addr):
    """Returns the country for a given ip address"""
    gi = GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE)
    response = {'ipaddr': ip_addr[:15],
                'country': gi.country_code_by_addr(ip_addr[:15])}
    return Response(json.dumps(response),  mimetype='application/json')

@app.route("/country_by_name/<domain_name>")
def country_by_name(domain_name):
    """Returns the country for a given domain name"""
    gi = GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE)
    response = {'domainname': domain_name[:70],
                'country': gi.country_code_by_name(domain_name[:70])}
    return Response(json.dumps(response),  mimetype='application/json')

if __name__ == "__main__":
    app.run(port=5000, host="0.0.0.0")

This example API service has three endpoints, each defined by the @app.route() decorator function. they are:

/
/country_by_ip/<ip_addr>
/country_by_name/<domain_name>

The first one just prints a little message about the other two, but those other two actually do the work of converting IP addresses or domain names into their associated countries.

Why is this example only a toy?

Well, the first line is a shebang, which bash uses to help the script run on the command line. And the last line, tells Flask to use it’s built in server to serve the API. The Flask documentation specifically cautions against this and there’s a whole section of the docs devoted to deploying flask apps. Obviously it seems like I’m ignoring the official advice, but this API isn’t destined for a production environment, it’s destined to run in my house, so it’s OK that I don’t care!

Running the code

Since were using the built in web server and we added the shebang at the beginning, we can make the file executable and execute it directly.

To do that, we must first tell the Linux shell that the file is executable:

chmod +x geoapi.py

Then we can run the file to start the API:

./geoapi.py

Which would produce output similar to the following.

* Serving Flask app "geoapi" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

This means your API is running and ready to use.

How do you use it?

The easiest way to test your API is to access it in a web browser. Type the address into the navigation bar of your preferred browser - http://localhost:5000/

Note that if you’re doing this on a remote server, you’ll need to replace ’localhost’ with the hostname or IP address of the server. Don’t forget to really think twice about running this on the public internet, that’s not what it was designed for at all.

If everything is working you should see this in the browser window:

Geolocate by IP or domain name
/country_by_ip/<ip_addr>
/country_by_name/<domain_name>

That’s the help the we defined for the out of the function associated with the ‘/’ route, since the ‘/’ is the first part of our URL after the domain name and port.

You can test the other routes we defined by changing the URL.

For example, http://localhost:5000/country_by_name/flask.pocoo.org returns:

{"domainname": "flask.pocoo.org", "country": "DE"}

Or you could try an IP address in stead of a domain name, like this one http://localhost:5000/country_by_ip/104.196.200.5, which returns:

{"ipaddr": "104.196.200.5", "country": "US"}

Conclusion

Creating you own API’s is great fun in both Python and R and hopefully it’s not too much of a stretch to imagine incorporating the results of these sorts of APIs into your own programs. There are tools in all modern languages to pull in data from these sorts of APIs into your programs as well as to parse JSON, so the opportunities for combining best of breed technologies is numerous.

Anyway, hopefully you’re inspired to create an API or two of your own. If you do, be sure to let me know!