10 avril 2011

A very simple HTTP server with basic MVC in Python

This is a simple proof of concept of a very simple HTTP server implementing a very basic MVC model (actually only routing and controller) in Python.

We will use the BaseHTTPServer as the web server.

The controller base class


All the controller needs is a reference to the BaseHTTPServer so that it can access the web request and write out the response.

class Controller(object):

    def __init__(self, server):
        self.__server = server

    @property
    def server(self):
        return self.__server

The router


The router needs to manipulate the request and response but also instanciate the controllers, that is why it receives an instance of BaseHTTPServer.

The routes are defined by 3 components:

- A regular expression that must be matched to trigger the route
- A controller class to be instanciated
- A method name to be called on the controller

The tricky part here is to instanciate the controller and call a method on it.

class Router(object):

    def __init__(self, server):
        self.__routes = []
        self.__server = server

    def addRoute(self, regexp, controller, action):
        self.__routes.append({'regexp': regexp, 'controller': controller, 'action': action})
        
    def route(self, path):
        for route in self.__routes:
            if re.search(route['regexp'], path):
                cls = globals()[route['controller']]
                func = cls.__dict__[route['action']]
                obj = cls(self.__server)
                apply(func,(obj, ))
                return

        # Not found
        self.__server.send_response(404)
        self.__server.end_headers()


The request handler


All the request handler needs to do is to instanciate the Router and add to it the routes it needs to manage.

This simple request handler is only able to reply to GET requests.

class MyRequestHandler(BaseHTTPRequestHandler):

    def __init__(self, request, client_address, server):
        
        routes = [
            {'regexp': r'^/$', 'controller': 'HomeController', 'action': 'indexAction'},
            {'regexp': r'^/content/', 'controller': 'ContentController', 'action': 'showAction'}
        ]
        
        self.__router = Router(self)
        for route in routes:
            self.__router.addRoute(route['regexp'], route['controller'], route['action'])

        BaseHTTPRequestHandler.__init__(self, request, client_address, server)
    
    def do_GET(self):
        self.__router.route(self.path)

A simple Hello World controller


class HomeController(Controller):

    def __init__(self, server):
        Controller.__init__(self, server)

    def indexAction(self):
        self.server.send_response(200)
        self.server.send_header('Content-type', 'text/html')
        self.server.end_headers()
        self.server.wfile.write('Hello world')


By the way, in MVC a controller should not do View stuff, see "Going further with a templating engine" below to make the separation better using a view layer.

A controller rendering a file


This controller will serve a file in the "public/" directory. Be carefull there is not enough error checking in this code...

class ContentController(Controller):
    
    CONTENT_BASE_PATH = 'public/'

    def __init__(self, server):
        Controller.__init__(self, server)
        
    def showAction(self):
        filename = ContentController.CONTENT_BASE_PATH + self.server.path[9:]
        if os.access(filename, os.R_OK) and not os.path.isdir(filename):
            #TODO: is there any possibility to access files outside the root with ..?
            file = open(filename, "r")
            content = file.read()
            file.close()
            
            #TODO: set correct content type
            self.server.send_response(200)
            self.server.send_header('Content-type', 'text/html')
            self.server.end_headers()
            self.server.wfile.write(content)
        else:
            self.server.send_response(404)
            self.server.end_headers()

Running the server


What we have defined here is a simple http server that will reply "Hello world" when the URL "/" is accessed, and return the content of a file named myFile (if it exists) when the URL "/content/myFile" is reached.

def main():
    try:
        httpd = HTTPServer(('', 8000), MyRequestHandler)
        print 'Server started...'
        httpd.serve_forever()
    except:
 print 'Server shutting down'
 httpd.socket.close()

if __name__ == '__main__':
    main()

Going further with a templating engine


It is quite simple to integrate a templating engine like Cheetah in this code.

Here is a example of controller rendering a template:

class TemplateController(Controller):

    def __init__(self, server):
        Controller.__init__(self, server)

    def listAction(self):
        self.server.send_response(200)
        self.server.send_header('Content-type', 'text/html')
        self.server.end_headers()
        self.server.wfile.write(Template ( file = 'templates/hello_world.tmpl', searchList = [{ }] ))
        return

Conclusion


That's the reason why I love Python so much, in very few lines we could implement basic MVC over a simple web server.

Aucun commentaire:

Enregistrer un commentaire