Building simple url-redirection service using Flask and Python

There are many technologies that can be used for building back-ends for web-sites, web-services and etc. In this post, I want to show how easily web-services can be created using Python and Flask. I don’t really like writing UI (or HTML) so I will avoid it by making the management of the urls database¬†available using RESTful API.

From the Flask’s official website:

Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions

In shorter words, Flask gives you the ability to get HTTP requests, redirect them to the appropriate handle that is defined for the specific path in the python application, do what ever you need to do, and generate a response that will be sent back as HTTP response to the requester (probably web-browser).

For the example, we’ll use PickleDb as our database (that’s what google gave me as a first result for “simple key value database for python”). From the official website:

pickleDB is a lightweight and simple key-value store. It is built upon Python’s simplejson module and was inspired by redis

So what do we need for our service ?

  1. Redirection management:
    • Create new redirections – the key will be an alias for the redirection (or can be an auto-generated unique id) and the value will be the actual redirection address.
    • Delete existing redirections.
  2. Make actual redirections – redirect the given key to the appropriate value (in case it exists).
  3. Security and Authentication – All the management system should be protected so only authorized users can manage redirections.

Let’s start with installing the pre-requirements:

pip install flask
pip install pickledb

Now, let’s create a new Flask application and run it from the main entry to the script:

from flask import Flask, request, abort
app = Flask(__name__)

if __name__ == "__main__":
    app.run()

In order to “hide” the database implementation from the application, let’s create a class that will manage data persistence:

class RedirectionsStore(object):
    DATABASE_PATH = "redirections.db"

    def __init__(self):
        # True asks the database to dump every change directly to the underlying file
        self.db = pickledb.load(RedirectionsStore.DATABASE_PATH, True)

    def add_record(self, key, value):
        if self.db.get(key) != None:
            return False

        self.db.set(key, value)
        return True

    def get_record(self, key):
        return self.db.get(key)

    def delete_record(self, key):
        self.db.rem(key)

    def get_all_records(self):
        return self.db.getall()

The management operations we want to allow are creating and deleting redirections. In order to do that let’s create an app route called “/mgmt” and implement POST and DELETE requests for it.

POST request will be used for adding new redirection and it’s implementation looks like this:

def add_new_redirection_request():
    # Try parsing base64 key and value
    try:
        key = base64.b64decode(request.json["key"])
        value = base64.b64decode(request.json["value"])
    except Exception as e:
        logger.error("Failed parsing data: %s" % e.message)
        abort(400) # Bad request

    logger.info("Adding redirection with key=%s, value=%s" % (key, value))
    db = RedirectionsStore()
    added = db.add_record(key, value)
    if not(added):
        logger.error("Key already exists - discarding request (key=%s)" % key)
        abort(403) # Forbidden

    return ("", 201) # Created

The post request should contain “key” and “value” arguments, base64 encoded and in JSON format. The function is simple, we’re trying to get the arguments, in case we fail we’ll respond with error code 400, in case the key already exists in the database we’ll respond with error code 403, otherwise we’ll store the key-value in the database and respond with error code 201 (you can read more about HTTP error codes here).

The redirection deletion method should be simple enough as well:

def delete_redirection_request():
    # Try parsing base64 key
    try:
        key = base64.b64decode(request.json["key"])
    except Exception as e:
        logger.error("Failed parsing data: %s" % e.message)
        abort(400) # Bad request

    logger.info("Deleting redirection with key=%s" % key)
    db = RedirectionsStore()
    db.delete_record(key)
    return ("", 204)

In order to route those two functions to “/mgmt” we’ll do the following:

@app.route("/mgmt", methods=[ "POST", "DELETE" ])
def api_mgmt():
    # Make sure we receive arguments with json format
    if not(request.json):
        logger.warn("Got mgmt API request not in json format - discarding!")
        abort(415) # Unsupported media type

    if request.method == "POST":
        logger.debug("Handling mgmt POST request")
        return ServerImplementation.add_new_redirection_request()

    if request.method == "DELETE":
        logger.debug("Handling mgmt DELETE request")
        return ServerImplementation.delete_redirection_request()

    logger.warn("Got mgmt request that cannot be handled")
    abort(400) # Bad request

We can print all the existing redirections by creating another route:

@app.route("/redirections", methods=[ "GET" ])
def api_redirections():
    logger.info("Got a request to list all redirections from database")

    db = RedirectionsStore()
    records = db.get_all_records()
    result = json.dumps(records)
    return (result, 200)

My favorite REST client for chrome is Postman and I’ll be using it for testing the API:

Create new redirection named “blah” to http://google.com

post-man-create-resource-success.PNG

Delete redirection named “blah”:

post-man-delete-resource.PNG

For the example, I’ll create some more redirections and list them using the “/redirections” API:

list-redirections.PNG

Now what left is implementing the url redirection itself:

@app.route("/redirect/<path:key>")
def redirect_request(key):
    logger.info("Got a redirection request with key=%s" % key)

    db = RedirectionsStore()
    result = db.get_record(key)
    if result == None:
        logger.error("Key %s has no redirection defined" % key)
        abort(400) # Bad request

    logger.debug("Redirecting to %s" % result)
    return redirect(result, 302)

Browsing to http://localhost:5000/redirect/search brought me to google.com and browsing to http://localhost:5000/redirect/unknown gave me the following error page:

bad-request.PNG

The last thing left is adding some basic authentication for the management API. There are multiple things that can be done with Flask, the simple one is defining user name and password and verifying it for each request:

def secure_api(f):
@wraps(f)
    def implementation(*args, **kwargs):
        auth = request.authorization
        if not(auth):
            logger.error("No authorization supplied, discarding request!")
            abort(401) # Unauthorized

        if (auth.username != "admin" or auth.password != "password"):
            logger.error("Bad user name or password (username=%s, password=%s)" % (auth.username, auth.password))
            abort(401) # Unauthorized

        return f(*args, **kwargs)

    return implementation

and what left is add “@secure_api” for every routed method we want to protect:

@app.route("/mgmt", methods=[ "POST", "DELETE" ])
@secure_api
def api_mgmt():
...

@app.route("/redirections", methods=[ "GET" ])
@secure_api
def api_redirections():
...

Sending GET request without providing credentials (or supplying wrong user name or password) will give us the following error:

list-redirections-access-denied.PNG

When re-sending the request with the right credentials will execute the request:

list-redirection-authenticated.PNG

In conclusion, Flask is an easy to use framework for implementing RESTful APIs and it can be used for many things. This is a very simple example without good handling for performance or scale but I guess it demonstrate the power of this framework.

The full source code can be found here and as usual, feel free to use it.

Alexander.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s