Translate

domingo, 26 de enero de 2014

Web en Apache con Python y WSGI (III) - Enrutador

Si has llegado hasta aquí sin pasar por el primer artículo 'Web en Apache con Python y WSGI (I)', no deberías continuar hasta leer los dos anteriores. Aunque todos los artículos son independientes en el tema a tratar, guardan una conexión con el objetivo hacia el que se dirigen PYTHON+MOD_WSGI+MYSQL+APACHE.

Introducción al concepto

Un "ENRUTADOR" de solicitudes web, tiene la importante tarea de gestionar qué respuesta se debe producir cuando se recibe una solicitud desde una página web. En la escueta definición de SOLICITUD se incluyen los clasicos FORM html, AJAX, REST,...y un largo etc. que ahora no corresponde detallar.

Esta pieza es fundamental en el desarrollo de un sitio web ( website ) que puede agrupar una gran cantidad de información y necesita responder dinámicamente según la solicitud que reciba de un cliente ( navegador, robot y otro tipo de programa o dispositivo).

Para desarrollar un 'ENRUTADOR' sencillo (para complicarse la vida, siempre hay tiempo...) estudiaremos que tipo de solicitudes podemos esperar y que tipo de respuestas vamos a producir.

Entradas y Salidas

Si te paras a pensar qué tipo de solicitudes puede hacernos un navegador cuando accede a nuestro sitio web, podríamos indicar algunas como :

http://www.prueba.com
http://www.prueba.com/cliente
http://www.prueba.com/cliente?codigo=9214&nombre=Jose%20Garcia      ( una tipica solicitud GET )
http://www.prueba.com/cliente/facturas/201400001

Además, HTTP define varias operaciones o verbos : GET, PUT, POST, DELETE que deberíamos tener en cuenta. Que pueden facilitarnos más información sobre qué tipo de operación debemos realizar con la solicitud, no olvidemos que junto a una solicitud, tambien podemos recibir datos.

GET  nos indica una solicitud de información o bien que se responda con una página web. Es el verbo por defecto.
PUT puede ser utilizado cuando nos solicitan que modifiquemos unos datos en una ficha de cliente. Por supuesto, la solicitud incluirá los datos a modificar y un código por el que deberíamos buscar al cliente.
POST puede ser utilizado cuando nos solicitan el crear un nuevo cliente.
DELETE puede ser utilizado cuando nos solicitan eliminar un cliente, siempre deberá incorporar los datos necesarios para identificar de una forma única al cliente a eliminar.

Observa que en cada información indico 'PUEDE SER UTILIZADO' porque esto es una convención, no una obligación.

También deberíamos tener en cuenta si nuestro sitio web responderá a solicitudes AJAX, creo que hoy día cualquier sitio web debe responder a solicitudes AJAX. Una solicitud AJAX transforma nuestra respuesta radicalmente.

Todo esto nos proporciona una valiosa información sobre qué se espera de 'ENRUTADOR'.

Definir un árbol de rutas posibles

 Observa el siguiente código :

# rutas ajax
mapper = routing.Map(default_subdomain='www',redirect_defaults='/')
mapper.add(rule='/dealer_ajax',controller="dealer.rest(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/user_ajax',controller="user.rest(environ['REQUEST_METHOD'].upper(),parametros)")
        mapper.add(rule='/manufacturer_ajax',controller="manufacturer.rest(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/supplier_ajax',controller="supplier.rest(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/customer_ajax',controller="customer.rest(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/customer_list_ajax',controller="customer.getList(parametros)")
mapper.add(rule='/product_ajax',controller="customer.get()")
mapper.add(rule='/user_auth_ajax',controller="user.auth(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/customer_auth_ajax',controller="customer.auth(environ['REQUEST_METHOD'].upper(),parametros)")
 

# rutas NO ajax
mapper.add(rule='/',controller="index()")

mapper.add(rule='/index',controller="index()")
mapper.add(rule='/admin',controller="admin()")
mapper.add(rule='/dealer',controller="dealer.get(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/user',controller="user.get(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/manufacturer',controller="manufacturer.get(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/supplier',controller="supplier.get(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/customer',controller="customer.get(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/customer_list',controller="customer.getList()")
mapper.add(rule='/customers',controller="customer.getList2()")
mapper.add(rule='/product',controller="product.get(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/user_auth',controller="user.auth(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/customer_auth',controller="customer.auth(environ['REQUEST_METHOD'].upper(),parametros)")
mapper.add(rule='/upload',controller="entity.rest(environ['REQUEST_METHOD'].upper(),parametros)")

Inicialmente defino una ruta por defecto, que respondería con una página inicial del sitio web.

Posteriormente voy incorporando distintas solicitudes posibles utilizando, el parámetro REQUEST_METHOD indica el tipo de verbo http GET, PUT, POST, DELETE y parámetros puede una estructura de datos :  list, dict o json.

mapper.add(rule=' solicitud posible ', controller=" función o clase.método que responderá ")

Además, es posible que con la solicitud nos encontremos con parámetros indeseables que el usuario haya introducido a mano en la linea de navegación y para los que no hayamos preparado el 'ENRUTADOR' :

http://www.prueba.com/customer?xyzk

Antes de comenzar a tratar la solicitud recibida, deberá eliminar los parámetros indeseables que pueden hacer que el ENRUTADOR fracase. En nuestro caso, solo he tratado de eliminar cualquier parámetro que aparezca detrás de un ? o %3F, si aún así, la ruta no existe por cualquier causa, devolvemos un código 404.

if environ['REQUEST_URI'].find('%3F')>0:
        __output = mapper.match(environ['REQUEST_URI'][0:environ['REQUEST_URI'].find('%3F')])
 elif environ['REQUEST_URI'].find('?')>0:
        __output = mapper.match(environ['REQUEST_URI'][0:environ['REQUEST_URI'].find('?')])
 else:
        __output = mapper.match(environ['REQUEST_URI'])
        # Si la ruta no existe devolvera 404 y podremos decidir tratarla o no


Pero suponga que lo que el cliente necesita con su solicitud es un documento que Vd. guarda en un repositorio del sitio web, en ese caso le puede interesar dejar que acceda a él libremente :

if __output == '404':
        # Realizar directamente la peticion no parametrizada en rutas
        no_mapper = eval("noMapper(environ['REQUEST_URI'])")
        if no_mapper == '404':
               # No existe la ruta parametrizada y tampoco el recurso
               __output = mapper.match('/')
               # evita codigo malicioso en eval {'__import__': None, '__builtins__': None}, {}
               output.append(eval(__output))
        else:
               # existe ruta directa al documento
               output.append(no_mapper)

Devolver una respuesta al cliente

Una vez tenemos todo parametrizado, solo queda generar la respuesta y eso lo conseguimos con :

if __output == '404':
       ...
else:
        if __output:
            try:
                 output.append(eval(__output))
            except Exception as e:
                 tb = sys.exc_info()[2]
                 LOG.debug("Error <%s> en linea %s !!!\n" % (str(e),tb.tb_lineno))



Por tanto, los datos que devuelva la función o clase.método, se incorporan a una estructura list denominada 'output'

Y esta estructura se devolverá acompañada de unos parámetros :

try:
        if len(output)>0:
            status = '200 OK'
            try:
                  output_len = len('\n'.join(output))
            except Exception as e:
                  tb = sys.exc_info()[2]
                  LOG.debug("Error <%s> en línea %s !!!\n" % (str(e),tb.tb_lineno))
            start_response(status, [('Content-type', 'text/html; charset=UTF-8'),
                                    ('Content-Length', str(output_len)),
                                    ('Set-Cookie',_set_cookie_.encode('utf8'))])
            return ' '.join(output)

Inicialmente comprobamos si 'output' contiene información, y posteriormente valora su longitud. Crea los datos para 'start_response' y devuelve ''.join(output).

En estas fechas ya he liberado la primera versión en desarrollo de PYMETRICK donde podrá encontrar un módulo ROUTING.py, además de otros muchos de los que hablaremos.

Conclusiones

Todo este código puede insertarse en el módulo tratado en el primer artículo Web en Apache con Python y WSGI (I), en el que nos referíamos a un fichero app.wsgi 

En el caso de las solicitudes AJAX, la respuesta debería contener una estructura de datos que posteriormente pueda interpretar y tratar javascript - en otra entrega escribiré sobre este particular - pero por el momento, puedes estar preguntándote ¿Dónde puedo comprobar si se trata de una solicitud AJAX ?, deberá comprobar si existe la variable HTTP_X_REQUESTED_WITH en el diccionario 'environ',  si no existe, señal de que no es una petición AJAX.

def  isAjax(self,environ):
        """AJAX, la variable HTTP_X_REQUESTED_WITH puede estar ausente en variables de entorno"""
        try:
                if environ.get('HTTP_X_REQUESTED_WITH').upper() == 'XMLHTTPREQUEST':
                    return 1
        except Exception as e:
                return 0