Sunday, November 22, 2015

How To Write a Python Web Framework From Scratch

http://funwithlinux.net/2015/11/how-to-write-a-python-web-framework-from-scratch

In recent years, Python has become a very popular web-programming language.  Unlike PHP, how to go about writing a web application is a little less straight forward in Python.  Most administrators are familiar with the LAMP stack, but there does not seem to a defacto standard in the Python world.  In this article, I’ll break down the different layers of the Python web stack (on Linux, of course), as well as how to start your own framework.





The Basic Stack

Python Web Frameworks exist in various stacks.  Apache + mod_python, Apache + mod_wsgi, Nginx + gunicorn, the list goes on.  I’m going to cover what I think is the best out of the box performance setup: nginx + uWSGI.
nginx is a very popular event-driven web server.  I’m not going to nginx in this guide other than to say I recommend using uwsgi_pass of nginx to proxy your web requests to uWSGI.  Please see uWSGI’s documentation for a good how-to on this point.
uWSGI is a Web Server that implements the python WSGI standard.  https://www.python.org/dev/peps/pep-0333/ uWSGI also implements the uwsgi protocol to communicate with nginx or other proxy software.  You can read more about uWSGI here:  https://uwsgi-docs.readthedocs.org/en/latest/
uWSGI is a fully function webserver, much like Apache and nginx, however in my experience using nginx to proxy requests to uWSGI results in more req/s to the client.
Please keep in mind, in this guide, uWSGI =/= WSGI.  uWSGI is a webserver that implements WSGI, python’s “Web Server Gateway Interface” standard.

How WSGI works

A webserver which implements WSGI talks to an application that supports WSGI.  Most modern Python frameworks support WSGI, and it is the preferred method of implementing a python web framework, at least as far as the folks at python are concerned.  In my understanding, this basically amounts to an application that defines an object, that object implements an instance method of ‘__call__’, and call accepts two inputs, ‘environ’ and ‘start_response’.
‘environ’ is a dictionary object with what we can consider environment variables in the web context; these are not Linux environment variables.  These include the server name, query string, path, user agent, and all manners of elements that the WSGI has passed to us.
‘start_response’ is a callback function that an application invokes to return status and header information to the calling software.  The start_response callback takes two inputs:  ’status’ and ‘headers’
‘status’ is a string representation of an HTTP status, such as ’200 OK’
‘headers’ appears to be a list of two-value tuples.  This is commonly built as a dictionary, and then converted using dictionary’s items() method.
Let’s look at a working example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import json
import uuid
 
class MyAPI:
    '''This class implements a bare minimum WSGI application'''
 
    def __init__(self):
       '''This method is optional, but is called whenever an object
          is created in python'''
 
       #We are creating a unique identifier to demonstrate that
       #multiple calls to the web service should have the same
       #uuid.
       self.myid = uuid.uuid1()
 
    def __call__(self, env, start_response):
        '''This method allows our application to support WSGI'''
 
        bdata = {
            'status'  : 'OK'
        }
 
        status = '200 OK'
        #uncomment the next line to see all the environment variables provided
        #print env.items()
 
        #env['wsgi.input'] is a python socket object provided by WSGI
        #if the request method was a posted JSON doc, it will be here.
        stream = env['wsgi.input']
 
        #if we determine the request body has a length
        #this is how we read that data.
        if env['CONTENT_LENGTH']:
            posted_body = stream.read(int(env['CONTENT_LENGTH']))
            #print posted_body
 
        #PATH_INFO is everything after the TLD (.com, .net, etc)
        #but before the query string (?abc=xyz)
        if (env['PATH_INFO'] == '/mypath'):
            bdata['path_info'] = 'correct path'
        else:
            bdata['path_info'] = 'incorrect path'
 
        #the query string is also called 'get data' in PHP or similar
        bdata['query_string'] = env['QUERY_STRING']
 
        headers = {}
        headers['Content-Type'] = 'application/json'
 
        #we're encoding our bdata dictionary object to json in utf-8 encoding
        #If we weren't sending JSON, we'd send a string instead.
        body = json.dumps(bdata, encoding='utf-8')
 
        #this will display in the terminal of our server
        #to demonstrate that we have the same object being reused after starting.
        print self.myid
 
        #This is how to set content length
        #this field in the header is typically required.
        headers['Content-Length'] = str(len(body))
 
        #We call the callback to send status and headers to the WSGI server
        start_response(status, headers.items())
 
        #We return the body of the response to the WSGI server.
        return body
 
#we create api as the object uWSGI will instantiate later
api = MyAPI()
So there we have it.  The above code is a working example of an application that supports WSGI, specifically one that will accept a JSON document posted in the body of the request, and reply with application/json type.  This is useful for building a simple REST-like framework.  Hopefully you read the comments and understood their meaning.
It’s worth noting that this example is technically an application.  In order for it to be a framework, it should implement some generic functions that can be reused later, such as URL routing, cookies, and other features typically found in a framework.  However, I feel like this example provides you with the basic building blocks of how to start a python web framework.

Calling the Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#this is python's built-in reference WSGI webserver
#it is highly useful for testing, but there are much
#better deployment options for production
from wsgiref import simple_server
 
import myapp
 
def run():
    api = myapp.MyAPI()
    httpd = simple_server.make_server('127.0.0.1', 8000, api)
    httpd.serve_forever()
 
if __name__ == '__main__':
    run()
As you can see here, we’re importing myapp, instantiating a new object of type myapp.MyAPI(), and passing that object as a reference to our wsgiref simple_server.
What’s really happening here?  I’ll try to cover the basic setps.
our object httpd binds to the specified IP address and port (aka, binding to a socket).  After binding to that socket, it is polling it for incoming requests.  When it receives an incoming request, it knows to call the api object we created.
If you have installed and configured uWSGI, you could start it with the following:
uwsgi –http :9090 -w myapp:api
uWSGI handles the socket binding instead of wsgiref.simple_serve.  uWSGI and wsgiref are both implementations of Python’s WSGI, although uWSGI also has a lot of other interesting features beyond that specification.

Deploying behind nginx

I really feel like uWSGI has great documentation on this.  Please see their quick-start guide as I won’t be duplicating that information here.

Exercises for the Reader

You will probably want to implement different methods for different request types, such as POST or GET.  You can use the environment variable ‘REQUEST_METHOD’ as follows:
1
if env['REQUEST_METHOD'] == 'POST':
Typically, you’ll want different URI’s to go to different methods as well.

Special Thanks

I would like to say thanks to flask, http://flask.pocoo.org/ and falcon, http://falconframework.org/
These two python frameworks are simple to use, provide good documentation, and have aided in my understanding of WSGI applications.
If you don’t want to implement your own framework, and want something on the light-weight side, I highly recommend investigating those two projects.
in How-To
, , ,
You can skip to the end and leave a response. Pinging is currently not allowed.

No comments:

Post a Comment