Twisted: web y base de datos



Versión: 2.6, 2.7.
Descargas: ejemplos.zip.

Entre la gran cantidad de herramientas integradas en Twisted se encuentran el paquete twisted.web y el módulo adbapi, que permiten el desarrollo de sitios web e interacción con base de datos de la forma más óptima y al estilo Twisted; es decir, sin bloquear la ejecución del bucle principal.

Comenzamos con una explicación de ambas herramientas y luego las llevamos, en conjunto (razón por la cual decidí integrarlas en un mismo artículo), a la práctica con un ejemplo funcional.

Se recomienda conocimiento básico sobre:

Twisted Web

Este paquete incluye todas las herramientas fundamentales para llevar a cabo una web al momento de desarrollar con Twisted. Sin embargo, para proyectos web de mayor complejidad considera utilizar un web framework como Django, Pyramid, web2py, etc. De todas maneras, será suficiente para nuestro ejemplo de registro de usuarios al final del artículo.

Contenido estático

El servidor web de Twisted es capaz de proveer contenido estático y generar páginas dinámicas (soporta las tecnologías CGI y WSGI). La forma más simple de poner en funcionamiento un sitio de contenido estático es invocando directamente a la herramienta twistd.

twistd -n web --path /html

De esta manera, Twisted inicializa un servidor web que escucha, por defecto, en el puerto 8080 y sirve el contenido estático dentro de la carpeta html. Esta última debe encontrarse en el directorio desde donde se ha invocado twistd. La forma de acceder a un supuesto archivo index.html dentro de la carpeta html es http://localhost:8080/html/index.html. Puede utilizarse . (punto) para indicar el directorio actual:

twistd -n web --path .

El acceso sería, entonces, http://localhost:8080/index.html.

Usuarios de Microsoft Windows probablemente deban invocar a twistd especificando su ruta completa:

C:\PythonXY\scripts\twistd -n web --path .

(En donde XY representa la versión, por ejemplo, Python27.)

Se obtiene un funcionamiento equivalente con el código siguiente.

from twisted.internet import reactor
from twisted.web.server import Site
from twisted.web.static import File

resource = File(".")
factory = Site(resource)
reactor.listenTCP(8080, factory)
reactor.run()

El reactor consiste en el bucle principal que acepta y rechaza conexiones, lee y escribe datos. La clase Site implementa el protocolo HTTP y File conecta al directorio o archivo especificado con dicho protocolo.

Es posible definir URLs que retornen un contenido específico. Es necesario importar la clase Resource que representa cualquier contenido al que se accede a través de una URL.

from twisted.web.resource import Resource

Luego, creamos una instancia y especificamos una URL para conectarla con un archivo (en lugar de la quinta línea del código anterior).

resource = Resource()
resource.putChild("index", File("html/index.html"))

Así, la URL http://localhost:8080/index retorna el contenido del archivo html/index.html. Y de forma similar para los directorios:

resource.putChild("static", File("html"))

static/* accede al contenido de la carpeta html.

Contenido dinámico

La forma de servir contenido dinámico es similar al método anterior. Se crea un recurso y se define el método render_GET, el cual será llamado ante este tipo de peticiones y el valor de retorno será mostrado en pantalla.

class DynamicPage(Resource):
    
    def render_GET(self, request):
        return "<html><h2>{0}</h2></html>".format(python_version())

En la cabecera del archivo importamos la función python_version.

from platform import python_version

Y por último se la asignamos a una URL específica.

resource = Resource()
resource.putChild("version", DynamicPage())

Al acceder a http://localhost:8080/version se mostrará en pantalla la versión de Python que estés corriendo. Para especificar la dirección principal se utiliza una cadena vacía.

resource.putChild("", DynamicPage())

Los parámetros que se incluyan en la URL se encuentran en request.args. Por ejemplo, si se accede a http://localhost:8080/version?a=1&b=2&c=3 entonces request.args es un diccionario con el valor {'a': ['1'], 'c': ['3'], 'b': ['2']}, de modo que el valor de a es obtenido utilizando request.args["a"][0].

Para URLs dinámicas debe crearse un recurso principal que determine las acciones que deben tomarse de acuerdo a la dirección ingresada. A modo dejemplo, queremos que nuestro sitio salude a los visitantes cuando ingresan su nombre de la forma http://localhost:8080/Nombre. Para esto, definimos el recurso que dará el mensaje como WelcomePage.

class WelcomePage(Resource):
    
    def __init__(self, name):
        Resource.__init__(self)
        self.name = name
    
    def render_GET(self, request):
        return "<html><h2>Hola, {0}!</h2></html>".format(self.name)

Ahora bien, ¿quién es el encargado de llamar a este recurso y pasar como argumento el nombre que se ha ingresado en la URL? Para esta tarea implementamos el recurso principal.

class MainResource(Resource):
    
    def getChild(self, name, request):
        return WelcomePage(name)


factory = Site(MainResource())

WelcomePage es un subrecurso del recurso principal, MainResource. Si se accede a http://localhost:8080/Nombre/Apellido entonces la función WelcomePage.getChild será llamada y el argumento name será "Apellido".

Base de datos

El problema con la utilización convencional de base de datos radica en que las operaciones suelen bloquear la ejecución del programa. Esto impediría que el reactor continúe atendiendo otros eventos mientras el módulo de base de datos aguarda al resultado de una consulta. Como solución Twisted provee el módulo adbapi (Asynchronous Database API), que actúa como una envoltura para cualquier módulo de base de datos que respete los estándares de la DB-API. Por ejemplo, considerando el siguiente código:

import sqlite3

conn = sqlite3.connect("database.db")
cursor = conn.cursor()

La implementación análoga en un proyecto de Twisted sería:

from twisted.enterprise import adbapi

dbpool = adbapi.ConnectionPool("sqlite3", "database.db")

Como primer argumento se indica el nombre del módulo que se desea utilizar, en forma de cadena y sin necesidad de importarlo previamente. Los argumentos remanentes serán pasados directamente a la función sqlite3.connect, incluyendo argumentos por nombre. Por lo tanto, los mismos dependerán del módulo utilizado (sqlite3, pymysql, MySQLdb, psycopg). No es necesaria la creación de un cursor, pues las consultas se ejecutan utilizando la función runQuery.

dbpool.runQuery("SELECT * FROM table1")

Esta función envía los argumentos directamente a sqlite3.Connection.execute(). Al igual que la función anterior, los valores dependen particularmente de cada módulo. El valor de retorno es un deferred. Si la consulta retorna algún valor, entonces será conveniente asociarlo con una callback para acceder al mismo y, opcionalmente, una errback en caso que la consulta falle.

def succeed(results):
    print "Resultados de la consulta:"
    for result in results:
        print result

d = dbpool.runQuery("SELECT * FROM table1")
d.addCallback(succeed)

runQuery retorna inmediatamente. La función succeed será llamada por el reactor cuando la consulta haya terminado de modo que la ejecución no se bloquea. El parámetro results equivale al valor retornado por cursor.fetchall().

Ejemplos

Ya conocidos los métodos para desarrollar sitios web e interactuar con bases de datos en Twisted, a modo de ejemplo implementamos un registro de usuarios.

Primero, el archivo signin.html que contiene el formulario.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>
	<title>Registrarse</title>
	<meta http-equiv="content-type" content="text/html;charset=utf-8" />
</head>
<body>
	<h4>Registrarse</h4>
    <form method="post" action="signin">
        <table>
            <tr>
                <td>Usuario</td>
                <td><input type="text" name="username"/></td>
            </tr>
            <tr>
                <td>Email</td>
                <td><input type="text" name="email"/></td>
            </tr>
            <tr>
                <td>Contraseña</td>
                <td><input type="password" name="password"/></td>
            </tr>
            <tr>
                <td>Reingresar Contraseña</td>
                <td><input type="password" name="password2"/></td>
            </tr>
        </table>
        <br /><button type="submit">Crear usuario</button>
    </form>
</body>
</html>

Luego, una simple página en caso de tener que mostrar un mensaje de error (por ejemplo, no se han completado todos los campos), de nombre error.html.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>
	<title>Registrarse</title>
	<meta http-equiv="content-type" content="text/html;charset=utf-8" />
</head>
<body>
    <h4>Ha ocurrido un error</h4>
    {0}
    <br /><br />
    <button onclick="history.back();">Volver</button>
</body>
</html>

Por último, el archivo principal con nombre a elección.

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

from twisted.internet import reactor
from twisted.web.server import Site
from twisted.web.resource import Resource


class SignInPage(Resource):
    
    def _getErrorPage(self, message):
        with open("error.html") as f:
            return f.read().format(message)
    
    def render_GET(self, request):
        with open("signin.html") as f:
            return f.read()
    
    def render_POST(self, request):
        username = request.args["username"][0]
        email = request.args["email"][0]
        password = request.args["password"][0]
        password2 = request.args["password2"][0]
        
        if not username or not email or not password or not password2:
            return self._getErrorPage(
                "Por favor complete todos los campos.")
        elif password != password2:
            return self._getErrorPage("Las constraseñas no coinciden.")
        
        return "Su usuario ha sido creado."


root = Resource()
root.putChild("signin", SignInPage())
factory = Site(root)
reactor.listenTCP(8880, factory)
reactor.run()

La función render_POST es similar a render_GET, pero es llamada al recibir peticiones del tipo POST.

Luego de ejecutarlo, al ingresar a http://localhost:8080/signin se mostrará el formulario en pantalla.

Vista previa

El siguiente paso es crear una base de datos. Utilizaremos SQLite ya que se integra por defecto en Python. Desde una consola de Python abierta en el directorio de nuestra web, ejecutamos:

>>> import sqlite3
>>> conn = sqlite3.connect("users.db")
>>> cursor = conn.cursor()
>>> cursor.execute("CREATE TABLE users (name TEXT, email TEXT, password TEXT)")
>>> conn.commit()
>>> conn.close()

Una vez creada la base de datos users.db, para guardar los usuarios registrados, añadimos el siguiente código en la función render_POST previo al retorno.

        dbpool = adbapi.ConnectionPool("sqlite3", "users.db")
        d = dbpool.runQuery("INSERT INTO users VALUES (?, ?, ?)",
                            (username, email, password))

En la cabecera del archivo importamos el módulo necesario.

from twisted.enterprise import adbapi

Tanto runQuery como addCallback retornan inmediatamente, por lo que sería incorrecto retornar "Su usuario ha sido creado." pues puede que aún no se haya insertado la información del usuario en la base de datos. En este tipo de casos, Twisted permite retornar NOT_DONE_YET para indicarle que aún no debe enviarle la respuesta al cliente.

        d.addCallback(self._userCreated, request)
        
        return NOT_DONE_YET

Con previa importación:

from twisted.web.server import NOT_DONE_YET, Site

Por lo tanto, añadimos una callback a la consulta para que sea llamada al finalizar la operación.

    def _userCreated(self, ret, request):
        request.write("Su usuario ha sido creado.")
        request.finish()

Escribimos el resultado y al finalizar se lo indicamos a Twisted llamando a request.finish. Código completo:

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

from twisted.enterprise import adbapi
from twisted.internet import reactor
from twisted.web.server import NOT_DONE_YET, Site
from twisted.web.resource import Resource


class SignInPage(Resource):
    
    def _getErrorPage(self, message):
        with open("error.html") as f:
            return f.read().format(message)
    
    def _userCreated(self, ret, request):
        request.write("Su usuario ha sido creado.")
        request.finish()
    
    def render_GET(self, request):
        with open("signin.html") as f:
            return f.read()
    
    def render_POST(self, request):
        username = request.args["username"][0]
        email = request.args["email"][0]
        password = request.args["password"][0]
        password2 = request.args["password2"][0]
        
        if not username or not email or not password or not password2:
            return self._getErrorPage(
                "Por favor complete todos los campos.")
        elif password != password2:
            return self._getErrorPage("Las constraseñas no coinciden.")
        
        dbpool = adbapi.ConnectionPool("sqlite3", "users.db")
        d = dbpool.runQuery("INSERT INTO users VALUES (?, ?, ?)",
                            (username, email, password))
        d.addCallback(self._userCreated, request)
        
        return NOT_DONE_YET


root = Resource()
root.putChild("signin", SignInPage())
factory = Site(root)
reactor.listenTCP(8080, factory)
reactor.run()

Al completar el formulario se inserta el usuario en la base de datos.

Vista previa

Por último, para completar más el ejemplo, añadimos algunas funciones para chequear que el usuario especificado no exista.

class SignInPage(Resource):
    
    def _checkUserAvailability(self, not_available):
        if not_available:
            self._writeAndFinish("El usuario ya existe.")
        else:
            self._insertUser()
    
    def _createUser(self):
        d = self._dbpool.runQuery("SELECT name FROM users WHERE name=?",
                                  (self._username,))
        d.addCallback(self._checkUserAvailability)
    
    def _getErrorPage(self, message):
        with open("error.html") as f:
            return f.read().format(message)
    
    def _insertUser(self):
        d = self._dbpool.runQuery(
            "INSERT INTO users VALUES (?, ?, ?)",
            (self._username, self._email, self._password)
        )
        d.addCallback(self._userCreated)
    
    def _userCreated(self, ret):
        self._writeAndFinish("Su usuario ha sido creado.")
    
    def _writeAndFinish(self, message):
        self._request.write(message)
        self._request.finish()
    
    def render_GET(self, request):
        with open("signin.html") as f:
            return f.read()
    
    def render_POST(self, request):
        self._username = request.args["username"][0]
        self._email = request.args["email"][0]
        self._password = request.args["password"][0]
        password2 = request.args["password2"][0]
        
        if (not self._username or not self._email or
            not self._password or not password2):
            return self._getErrorPage(
                "Por favor complete todos los campos.")
        elif self._password != password2:
            return self._getErrorPage("Las constraseñas no coinciden.")
        
        self._request = request
        self._dbpool = adbapi.ConnectionPool("sqlite3", "users.db")
        self._createUser()
        
        return NOT_DONE_YET

Como puede observarse, las aplicaciones web y operaciones con base de datos en Twisted mantienen la misma política que el resto de sus herramientas: el flujo del programa se encuentra definido por los diversos eventos (_userCreated, _insertUser, _checkUserAvailability) y la ejecución nunca se detiene a la espera de un resultado u operación.



Deja un comentario