MagMax Blog

Aviso: En este blog puede encontrar código!

Python en La Web

| Comments

Recientemente se me están ocurriendo muchos micro-proyectos en red, y cada vez estoy más convencido que es interesante plantearse siempre la opción ReST, frente a otro tipo de middleware.

Podéis pensar: ¿Y me voy a crear todo un servidor web para esta tontería? Pues mi respuesta es que sí, ya que ReST ofrece muchas ventajas frente al resto:

  • Multilenguaje. Al fin y al cabo, consiste en mensajes de texto plano.
  • Sencillo. Se puede utilizar ncat para probarlo.
  • Versátil. Basta cambiar el content-type para soportar XML, JSON, YAML, JPG, Streaming, …

Vaaaaaale, en informática no existe una solución universal para todo y es muy probable que le encontremos problemas, como el tiempo de respuesta, que las operaciones no deberían tener estado, etc. pero, como he dicho, debe ser una opción a evaluar.

El caso es que vamos a crearnos una calculadora tremendamente potente en 30 líneas de código. Y el cliente tendrá la friolera de… 8 líneas. ¿Qué me decís ahora? ¿Merece la pena tenerlo en cuenta?

El servidor

Primero el código y luego lo vemos:

#!/usr/bin/python
# -*- coding:utf-8; tab-width:4; mode:python -*-
import SimpleHTTPServer
import SocketServer

class MyServer(SimpleHTTPServer.SimpleHTTPRequestHandler):
    def do_POST(self):
        self.process()

    def do_GET(self):
        self.process()

    def process(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(self.__response_body())

    def __response_body(self):
        return str(eval(self.__read_body()))

    def __read_body(self):
        length = int(self.headers.getheader('content-length'))
        return self.rfile.read(length)

if __name__=="__main__":
    port = 2349
    s=SocketServer.TCPServer(('',port),MyServer)
    print "ScriptServer running on port", port
    s.serve_forever()

Vamos con la parte principal: selecciono un puerto (el 2349) y me creo un SocketServer, indicándole que use como backend mi clase principal. Por razones estéticas, me imprimo el puerto y me pongo a escuchar (lo que se conoce como un “bucle de eventos”).

Mi clase principal hereda de SimpleHTTPRequestHandler, lo que me da resueltos casi todos mis problemas. Como soy muy indeciso, decido implementar tanto el método GET como el POST, llamando ambos a “process”.

Vamos a fijarnos en la parte más importante: mi método process. Sólo tiene 3 líneas pero son muy matonas. Copiémoslo de nuevo:

def process(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(self.__response_body())

En estas 3 líneas estamos construyendo la respuesta. Primero indico el código de respuesta (200 viene a ser un “todo va bien”).

Después indico que se han acabado las cabeceras. Las cabeceras se separan del cuerpo por un salto de línea, así que esta línea está imprimiendo un ‘\n’ nada más, pero es indispensable.

Finalmente, escribo en el buffer de salida la respuesta que quiero dar.

También es importante la lectura del cuerpo del mensaje. Lo copio de nuevo:

def __read_body(self):
        length = int(self.headers.getheader('content-length'))
        return self.rfile.read(length)

Primero tengo que obtener la longitud del stream y luego leerla. Al ser un stream abierto, si tratamos de leerlo entero nunca terminará, ya que el socket sigue abierto.

El resto es la implementación de mi “calculadora”, que veréis que no es más que una llamada a “eval”. No tiene mayor misterio.

El cliente

Y ahora vamos con el cliente. Primero el código:

#!/usr/bin/python
# -*- coding:utf-8; tab-width:4; mode:python -*-

import httplib
import unittest

class server_tester(unittest.TestCase):

    def send_operation(self, operation):
        connection = httplib.HTTPConnection('localhost', 2349, timeout=10)

        connection.request('GET', '/', operation)
        response = connection.getresponse()

        result = response.read()
        connection.close()

        return result

    def test_number(self):
        self.assertEqual('2', self.send_operation('2'))

    def test_sum(self):
        self.assertEqual('5', self.send_operation('2+3'))

    def test_pow(self):
        self.assertEqual('256', self.send_operation('2**8'))

    def test_error(self):
        self.assertEqual('', self.send_operation('esto no se puede calcular'))

    def test_strings(self):
        self.assertEqual('hello, world', self.send_operation('"hello, " + "world"'))


if __name__ == '__main__':
    unittest.main()

Sí, como habéis observado estoy probando mi servidor :D

Lo más importante de este código es la función send_operation, donde conecto con mi servidor, le envío una solicitud y devuelvo el body. No hace nada más.

El resto son comprobaciones varias demostrando la potencia de mi “calculadora”, ya que suma hasta cadenas XD

Más información

En esta ocasión sólo he utilizado la biblioteca estándar de python. No hay dependencias externas ni cosas raras. Así que lo mejor es ir a la propia documentación de python.

Comments