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:
|
|
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:
|
|
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
:
|
|
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 “https://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:
|
|
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:
|
|
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:
|
|
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:
|
|
Con esto podemos crear el archivo ’templates/example.html':
|
|
Y modificamos la clase de ejemplo:
|
|
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.