HTTP server boilerplate for AsyncIO

I am not going to create yet another http server for asyncio, rather, I will share some code pieces that I can create simple http endpoints for AsyncIO.

Wirlwind

It is a Python boilerplate class; it's open for modification upon your needs.


def http_document(content, status):
    return '\n'.join((
        f'HTTP/1.1 {status} OK',
        f'Content-Length: {len(content)}',
        'Content-Type: text/html',
        'Server: almost-an-nginx',
        '\r',
        content
    ))

def json_response(payload, status=200):
    return http_document(json.dumps(payload), status)

class Whirlwind:
    """
    Maybe todos:
        - Process the request headers
        - Parse querystrings
    """
    def __init__(self) -> None:
        self.routes = []

    def add_route(self, method, path, dispatcher):
        self.routes.append((method, path, dispatcher))

    def resolve_route(self, request_body):
        [first_line, *headers, request_body] = (request_body.split('\n'))
        [requested_method, requested_path, _] = first_line.split()
        for (method, path, dispatcher) in self.routes:
            if '?' in requested_path:
                [requested_path, *_] = requested_path.split('?')
            if method == requested_method.lower() and path == requested_path:
                return dispatcher, request_body

    async def serve(self, reader, writer):
        data = await reader.read(65537)
        body = data.decode()
        route = self.resolve_route(body)
        if route is None:
            writer.write(str.encode(json_response({'error': 'Not found.'}, 404)))
        else:
            dispatcher, request_body = route
            if request_body:
                result = await dispatcher(json.loads(request_body))    
            else:
                result = await dispatcher(request_body)
            writer.write(str.encode(json_response(result, 200)))

        await writer.drain()
        writer.close()

Lets use this class.

Simple analytics endpoint with Redis

I want to create an endpoint /hit which increments a key in redis store.

I will use aioredis redis client library.

async def get_somethings():
    ...

async def post_somethings(body):
    await redis.incr(body['key'])
    hits = int(await redis.get(body['key']))
    return {
        'hits': hits,
    }

async def main():
    application = Whirlwind()
    application.add_route('get', '/somethings', get_somethings)
    application.add_route('post', '/somethings', post_somethings)
    server = await asyncio.start_server(application.serve, '127.0.0.1', 8888)
    async with server:
        await server.serve_forever()

asyncio.run(main())

I am calling the "somethings" endpoint with post and "key" specificed in JSON body.

$ http post "127.0.0.1:8888/somethings?333" key=ttt
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/html
Server: almost-an-nginx

{
    "hits": 1
}

Lets try again and watch the result.

$ http post "127.0.0.1:8888/somethings?333" key=ttt
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/html
Server: almost-an-nginx

{
    "hits": 2
}

This is all. Happy hacking! :)