Simple dockerized Flask API to python program

Introduction

As a data scientist I write a lot of python code, but when the programs should go into production it is often somewhat tedious to set up the correct python environment and packages. Luckily, this problem have been solved for a while using Dockers, but how should other programs interface with the python program? A common solution for this problem is build an API and in this article I will show how very simple it is to create an API on top of the python application in Python. Additionally, the Flask setup I am about to show, also comes with a the very nice Swagger UI that allows users of the API understand and get familiar with the endpoints.

Overview of the files

All the files can be downloaded from: github.com/tjansson60/flask_api_example and the overall file structure is the following:

requirements.txt
Dockerfile
app:
   words.txt
   application.py
   main.py

So let’s go through the most important ones.

application.py
This is the main python application. I have written a simple function to create XKCD style passwords, but this could as any other arbitrarily complex function. The function I have written takes two arguments; a number of words and the amount of passwords to return. The list of words is a copy of /usr/share/dict/words from my locale Linux installation, but any list of words would do (preferably large).

import numpy as np
 
def generate_passwords(words=4, amount=10):
    '''Generates a XKCD style password with a number of words. Returns a list of amount passphrases '''
 
    # Get a list of words
    with open('words.txt') as f:
        wordlist = [word.strip().replace("'","") for word in f]
 
    # Return a list of passwords of strings
    passwords = [' '.join(list(np.random.choice(wordlist, words))) for i in range(amount)]
    return passwords
 
if __name__ == "__main__":
    print(generate_passwords())

main.py
With the amazing Flask library and the flask_restplus extension, it is dead simple to write an API. I define two routes: one without arguments and one with two arguments that are then passed on to the function above. When main.py is executed a debugging swagger UI is started on port 5000: http://127.0.0.1:5000. The screenshot above is from the Swagger UI. The Swagger UI lists all the available endpoints and provides both documentation and a playground to test the API with parameters.

from application import generate_passwords
from flask import Flask, jsonify
from flask_restplus import Resource, Api, Namespace
app = Flask(__name__)
api = Api(app)
 
pw_api = Namespace('passwords', description='XKCD style passwords')
api.add_namespace(pw_api)
 
# Create a REST resource
@pw_api.route('/', '/words/<int:words>/amount/<int:amount>')
class Password(Resource):
    def get(self, words=4, amount=10):
        return jsonify({"passwords": generate_passwords(words,amount)})
 
if __name__ == '__main__':
    app.run(debug=True)

The basic work is now done, but could be expanded as needed with more endpoints and perhaps some authentication, such as auth0:
auth0.com/blog/developing-restful-apis-with-python-and-flask/
but this is a bit beyond the scope of this simple introduction.

Docker container

It is not recommended to expose a Flask API publicly in production, but by wrapping the API in a Docker image with UWSGI and NGINX this problem is mitigated. The Docker image I refer to below use a special folder structure and this is why the app folder exists. The API should also be in a file main.py to adhere to this Docker image.

FROM tiangolo/uwsgi-nginx-flask:python3.6
 
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
 
COPY ./app /app

For those new to Docker this is probably to little information and for those experienced in Dockers this is not needed, but to put it shortly the following command builds the Docker image with the API inside:

$ docker build -t pw_api:0.1 .

To run a Docker container from using the image we have just build:

$ docker run -d -p 80:80 pw_api:0.1

The container can now be accessed in a browser on port 80: http://127.0.0.1/

Leave a Reply