Micro-framework web en Python


En esta ocasión necesito un pequeño servidor web. Nada de otro mundo. Poco más que el SimpleHTTPServer. Podría utilizar tornado, django, bottle o cualquiera de tantos... Pero no necesito tanta parafernalia y no quiero añadir más dependencias.

Y como parece que hoy en día todo el mundo tiene que crearse su propio framework web, pues aquí está el mío, ea.

Debo admitir que he aprendido muchas cosas sobre por qué se hacen como se hacen los otros frameworks.

En próximos artículos encontraremos usos para este micro-framework.

Lanzando el servidor

Venga, al lío. Éste será mi programa principal:

if __name__ == '__main__':
    server_address = ('', 8001)
    print 'serving at', server_address
    httpd = HTTPServer(server_address, HTTPRequestHandler)
    httpd.serve_forever()

Línea a línea:

  • sólo se ejecuta cuando es el programa principal y no cuando se ha importado.
  • creo la dirección donde escucharé: localhost y puerto 8001.
  • Imprimo el puerto en el que estoy escuchando (que después tengo mogollón de historias y no sé dónde está cada cual).
  • Creo mi servidor HTTP. Las peticiones las gestionará HTTPRequestHandler, que veremos más adelante.
  • Y a esperar eventos para siempre.

Y eso es todo lo que necesita mi programa principal. Vamos a gestionar las peticiones.

Manejando peticiones

Vamos a implementar ahora la clase HTTPRequestHandler que dejé colgada. Su función será la de un router, es decir, debe decidir quién va a gestionar la HTTPRequest.

Así que me voy a asociar expresiones regulares con manejadores, lo que resulta bastante sencillote. Para ello usaré una variable de clase. Hay maneras más bonitas de hacerlo... Pero me vale y no alarga el ejemplo:

class HTTPRequestHandler(BaseHTTPRequestHandler):
    HANDLERS = [
        (re.compile('/'), Example),
        ]

Como estoy heredando de BaseHTTPRequestHandler (que está en el módulo BaseHTTPServer, como luego veremos), debería implementar algún método de tipo callback, como do_GET:

class HTTPRequestHandler(BaseHTTPRequestHandler):
    HANDLERS = [
        (re.compile('/'), Example),
        ]

    def do_GET(self):
        url = urlparse(self.path)

        for regex, clazz in self.HANDLERS:
            m = regex.match(url.path)
            if m:
                handler = clazz(self)
                handler.get()
                return handler.finish()
        self.send_error(httplib.NOT_FOUND)

No os asustéis que no es para tanto. Además, seguro que ya controláis mogollón gracias al tutorial avanzado de Python.

El método do_GET hace uso de muchas variables que ya nos proporciona BaseHTTPRequestHandler. Nada más empezar, lee el path, que contiene URL completa. Hago uso de la librería estándar de Python con el método urlparse.urlparse que me divide la URL en sus distintas partes.

Ahora recorro el vector de expresiones regulares intentando que alguna de ellas encaje con el path. Por ejemplo, si mi URL era "http://www.example.org/any/thing", url.path contendrá sólo "/any/thing", que es lo que comparo con la URL.

Si la expresión regular tuvo éxito, creo un objeto del tipo asociado (clazz), pasándole la petición (es decir, la propia clase); llamo al método get del objeto que acabo de crear y me adelanto a lo que os enseñaré después llamando al método de finalización.

Si ninguna expresión regular se ajusta a la solicitud, lanzo un 404.

Sólo me queda asegurarme de que todas las clases asociadas a una expresión regular tienen, al menos, los métodos get() y finish() y que admiten la HTTPRequest en su inicializador.

Los Handlers

Así que me voy a crear un manejador básico:

class Handler(object):
    def __init__(self, request):
        self.request = request
        self._output = StringIO()
        self.write = self._output.write

    def finish(self):
        length = self._output.tell()
        self._output.seek(0)
        self.request.send_response(httplib.OK)
        encoding = sys.getfilesystemencoding()
        self.request.send_header("Content-type", "text/html; charset=%s" % encoding)
        self.request.send_header("Content-Length", str(length))
        self.request.end_headers()
        shutil.copyfileobj(self._output, self.request.wfile)
        self._output.close()

    def get(self):
        raise NotImplementedError()

El constructor hace 3 cosas importantes, para 3 líneas que tiene:

  • se guarda la HTTPRequest para luego
  • inicializa el búfer de salida. He decidido utilizar un StringIO, que se maneja como si fuera un fichero.
  • liga un método de clase a un método de mi búfer.

¿Cómo? ¿Que liga qué?

Sí, acordaos que Python funciona básicamente como un mogollón de tablas Hash, así que si proporciono un elemento callable que pertenece a mi clase, es como si añadiera un método. Esa línea es equivalente a lo siguiente:

    def write(self, *args, **kwargs):
        return self._output.write(*args, **kwargs)

Pero es mucho más sencillo, ¿no? Además, es algo más rápida porque no añade un nivel de llamadas a función.

El método finish() rebobina mi búfer, y lo manda como respuesta con un código 200, es decir, OK. La clase BaseHTTPRequestHandler es horrible y me obliga a un montón de parafernalias, como veis.

Finalmente añado el método get() que es abstracto y tendré que sobreescribir.

Todo el código

Y es todo lo que necesito... bueno, casi. Falta el temita de las plantillas, pero mi framework está terminado. Veamos todo el código junto:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import re
import httplib
import shutil
from StringIO import StringIO
from urlparse import urlparse

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler


class Handler(object):
    def __init__(self, request):
        self.request = request
        self._output = StringIO()
        self.write = self._output.write

    def finish(self):
        length = self._output.tell()
        self._output.seek(0)
        self.request.send_response(httplib.OK)
        encoding = sys.getfilesystemencoding()
        self.request.send_header("Content-type", "text/html; charset=%s" % encoding)
        self.request.send_header("Content-Length", str(length))
        self.request.end_headers()
        shutil.copyfileobj(self._output, self.request.wfile)
        self._output.close()

    def get(self):
        pass


class Example(Handler):
    def get(self):
        self.write('<html><body>Hello, world!!</body></html>')

class HTTPRequestHandler(BaseHTTPRequestHandler):
    HANDLERS = [
        (re.compile('/'), Example),
        ]

    def do_GET(self):
        url = urlparse(self.path)

        for regex, clazz in self.HANDLERS:
            m = regex.match(url.path)
            if m:
                handler = clazz(self)
                handler.get()
                return handler.finish()
        self.send_error(httplib.NOT_FOUND)


if __name__ == '__main__':
    server_address = ('', 8001)
    print 'serving at', server_address
    httpd = HTTPServer(server_address, HTTPRequestHandler)
    httpd.serve_forever()

A parte de los imports, lo único que se añade aquí es la clase Example que implementa mi pequeño Handler de ejemplo. Para ampliar mi site bastaría con añadir más expresiones regulares y más clases que hereden de Handler.

También faltaría hacer que gestionara otras acciones HTTP, como POST, HEADER, DELETE, etc... ¿Alguien se atrevería a implementarlo en un comentario de este post? No debería requierir más de 8 líneas de código. Repasad el tutorial avanzado de Python y veréis cómo tengo razón.

La clase de Ejemplo me ha quedado un poquitín fea... Así que voy a crear plantillas.

Añadiendo plantillas

Añadiremos lo siguiente:

from String import Template

template_path = 'templates'

class Handler(object):
    # Add this method:
    def render(self, filename, data={}):
        with open(os.path.join(template_path, filename)) as f:
            template = Template(f.read())
            self.write(template.safe_substitution(data))

Con esto podemos crear el archivo 'templates/example.html':

<html>
<body>
{greetings}
</body>
</html>

Y modificamos la clase de ejemplo:

class Example(Handler):
    def get(self):
        self.render('example.html', {greetings: 'Hello, world!!'})

Posibles mejoras

Hay muchos frameworks. Cada uno tiene unos puntos fuertes y unos débiles. Éste es bastante malo, pero para mis objetivos me basta.

Éstas son algunas de las ventajas que tienen otros frameworks y que podrían añadirse a éste:

  • Sistema de plantillas jerárquicas. Se podría implantar fácilmente utilizando jinja2
  • Gestión automática argumentos en el GET. Que las expresiones regulares generen grupos que pasan como argumento a la función del GET. Es fácil de implementar.
  • Gestión de idiomas, que podría implementarse mediante funciones a las que se llaman desde las plantillas.
  • Acceso a BBDD, que podría implementarse con sqlalchemy y no tendría nada que envidiar al sistema de django.
  • Entorno de pruebas, aunque se podría utilizar webunit.
  • Evitar el uso de BaseHTTPRequestHandler. Temo que su implementación podría ser más eficiente y se podrían ahorrar numerosas llamadas, obteniendo un sistema con mejor rendimiento.
  • Gestionar excepciones para enviar mensajes de error con un simple raise.
  • Mejorar el log.

Hay muchas cosas que se pueden hacer, pero hay que saber cuándo parar.

En futuros artículos veremos al menos dos usos de este micro-framework.


Comentarios

Comments powered by Disqus