Wednesday, May 11, 2011

WebApy -- webserver for easy and rapid REST API imitation

Have been developing an application which depends on a remote REST (stands for Representational State Transfer) API of one of popular services I ran into the need to use it [API] more intensively while testing/debugging the code. Not all remote services provide developers with SandBox`ed environments to play in. And not all services may tolerate frequently repeated requests to their REST API; they may just ban your access.

The best way to avoid such problems is to imitate remote API locally.

WebApy -- is a simple lightweight python Webserver built on the top on BaseHTTPServer. WebApy allows easily to create REST API which pretends like original one.

When it might required
  1. To develop Unit tests for a library/application which interacts with remote REST API;
  2. To debug such library/application more intensively and not being dependent on remote service availability;
  3. To make a fast dirty prototype of REST API for your service.
How it works
  1. Create a file (or several ones) called "hook" -- a regular Python file with pre-defined structure (the sample of such hook is available in hooks/ dir; "hooks" must have "hooks.py" filename extension);
  2. Implement canHandleRequest() static method which tells WebApy that the hook can handle this request;
  3. Implement code(), headers() and data() methods to return corresponding response values on the passed request;
  4. Run WebApy server instance to serve your application/library with imitated REST API.
Example
Last.Fm is world's largest and well known online music catalogue. It has a remote XML API to access to theirs music information database. Before start working with the API a client application should perform several authentication steps:
  1. Retrieve an Auth token;
  2. Retrieve an Auth session (providing user's credentials and obtained Auth token).
Lets see how to make WebApy hook which imitate the first step -- to provide a client with an Auth token.

Accordingly to Last.Fm developer's documentation an Auth token retrieving is performed via auth.getToken request. It has only one mandatory parameter -- "api_key" (a Last.Fm API key; it can be received upon request to Last.Fm).

Step 1
First we should check inside the hook if it can handle received API request. The hook can handle request if the following conditions are met:
  • The request is GET [type];
  • There is "method" argument passed;
  • The value of "method" argument is "auth.gettoken".

Lets implement canHandleRequest():

    @staticmethod
    def canHandleRequest(request):
        if "GET" != request.method:
            return False

        return "auth.gettoken" == request.simpleQuery.get("method", [None])[0]

Step 2
Second and final step: implement a logic to return a corresponding response to REST API client. Suppose only a client with Last.fm API key b25b959554ed76058ac220b7b2e0a026 is able to get Auth token. For other ones an error must be returned:

    def __makeResponse(self):
        apiKey = self.request.simpleQuery.get("api_key", [None])[0]

        if self.isValidApiKey(apiKey):
            self.__response = make_authenhicated_response(authToken = "cf45fe5a3e3cebe168480a086d7fe481")
        else:
            ### Error code "10" stands for invalid API key
            self.__response = make_failed_authenhicated_response(errorCode = 10)

The final code will look like:

import string
RESPONSE_TPL = string.Template("<?xml version=\"1.0\" encoding=\"utf-8\"?><lfm status=\"${code}\">${body}</lfm>")

def make_authenhicated_response(authToken):
    return RESPONSE_TPL.substitute(code = "ok", body = "<token>%s</token>" % authToken)

def make_failed_authenhicated_response(errorCode):
    return RESPONSE_TPL.substitute(code = errorCode, body = str())

class RequestHook:
    @staticmethod
    def canHandleRequest(request):
        if "GET" != request.method:
            return False

        return "auth.gettoken" == request.simpleQuery.get("method", [None])[0]

    def __init__(self):
        self.__response = None
        self.__makeResponse()
          
    def __makeResponse(self):
        apiKey = self.request.simpleQuery.get("api_key", [None])[0]

        if self.isValidApiKey(apiKey):
            self.__response = make_authenhicated_response(authToken = "cf45fe5a3e3cebe168480a086d7fe481")
        else:
            ### Error code "10" stands for invalid API key
            self.__response = make_failed_authenhicated_response(errorCode = 10)

    def code(self):
        return 200

    def headers(self):
        return {}

    def data(self):
        return self.__response

    @staticmethod
    def isValidApiKey(key):
        return "b25b959554ed76058ac220b7b2e0a026" == key

Seems pretty easy.

Test
Lets use "curl" utility to test how "our" API works. A malformed request:
$~ curl 'http://localhost:8080/?method=auth.gettoken'


And the correct one:
$~ curl 'http://localhost:8080/?method=auth.gettoken&api_key=b25b959554ed76058ac220b7b2e0a026'
cf45fe5a3e3cebe168480a086d7fe481

Distribution
WebApy REST API webserver can be directly downloaded from the git repository: http://git.thekondor.net/webapy.git. The software is licensed in terms of GNU GPL v3 and higher.

General notes
Imitated REST API can return JSON as well as XML responses (actually anything; depends on your needs).  WebApy is not intended to serve production environment, for debugging and testing purposes only since it was developed as an accessorial part of another project of mine. Hence it has some limitations and things to improve (especially I want to replace canHandleRequest() with the declarative description). Documentation is coming soon.

Anyway please feel free to submit your bug reports if any.

No comments:

Post a Comment