Servicios

El W3C define un servicio web como “un sistema de software diseñado para soportar interacción interoperable máquina a máquina sobre una red”. Ésta es una definición muy amplia, y encierra un largo número de protocolos diseñados, no para la comunicación máquina-humanos, sino para la comuniación máquina-máquina tal como XML, JSON, RSS, etc.

web2py proporciona desde el comienzo soporte para muchos protocolos, incluyendo XML, JSON, RSS, CSV, XMLRPC, JSONRPC, AMFRPC, y SOAP. web2py puede también extenderse para soportar protocolos adicionales.

Cada uno de estos protocolos se soporta en múltiples formas, y se hace una distinción entre:

  • Presentación de la salida de una función en un formato dado (por ejemplo XML, JSON, RSS, CSV)
  • Llamadas a procedimientos remotos (por ejemplo XMLRPC, JSONRPC, AMFRPC)

Presentando un diccionario

HTML, XML, y JSON

Considere la acción siguiente:

1
2
3
def count():
    session.counter = (session.counter or 0) + 1
    return dict(counter=session.counter, now=request.now)

Esta acción devuelve un contador que se incrementa en uno cuando un visitante recarga la página, y la fecha y hora de la solicitud de página actual.

Normalmente esta página sería solicitada a través de:

http://127.0.0.1:8000/app/default/count

y mostrada en HTML. Sin escribir una línea de código, le podemos pedir a web2py que presente esta página usando diferentes protocolos agregando una extensión a la URL:

http://127.0.0.1:8000/app/default/count.html http://127.0.0.1:8000/app/default/count.xml http://127.0.0.1:8000/app/default/count.json

El diccionario devuelto por la acción puede ser presentado en HTML, XML y JSON, respectivamente.

He aquí la salida XML:

1
2
3
4
<document>
   <counter>3</counter>
   <now>2009-08-01 13:00:00</now>
</document>

Y aquí la salida JSON:

1
{ 'counter':3, 'now':'2009-08-01 13:00:00' }

Note que los objetos date, time, y datetime se presentan como cadenas en formato ISO. Ésto no es parte del estándard JSON, sino mas bien una convención de web2py.

Vistas Geńericas

Cuando, por ejemplo, se llama a la extensión ”.xml”, web2py busca un archivo plantilla llamado “default/count.xml”, y si no lo encuentra, busca una plantilla llamada “generic.xml”. Los archivos “generic.html, “generic.xml”, “generic.json” son incluidos con la aplicación de andamiaje actual. Otras extensiones pueden ser definidas fácilmente por el usuario.

No es necesario hacer nada para habilitar esto en una aplicación de web2py. Para usarlas en una aplicación antigua de web2py, podría necesitar copiar los archivos “generic.*” de una aplicación de andamiaje más nueva (después de la versión 1.60).

Este es el código de “generic.html”

{{extend 'layout.html'}}
{{=BEAUTIFY(response._vars)}}
<button onclick="document.location='{{=URL("admin","default","design",
args=request.application)}}'">admin</button>
<button onclick="jQuery('#request').slideToggle()">request</button>
<div class="hidden" id="request"><h2>request</h2>{{=BEAUTIFY(request)}}</div>
<button onclick="jQuery('#session').slideToggle()">session</button>
<div class="hidden" id="session"><h2>session</h2>{{=BEAUTIFY(session)}}</div>
<button onclick="jQuery('#response').slideToggle()">response</button>
<div class="hidden" id="response"><h2>response</h2>{{=BEAUTIFY(response)}}</div>
<script>jQuery('.hidden').hide();</script>

Aquí está el código de “generic.xml”

1
2
3
4
5
6
7
8
{{
try:
   from gluon.serializers import xml
   response.write(xml(response._vars),escape=False)
   response.headers['Content-Type']='text/xml'
except:
   raise HTTP(405,'no xml')
}}

Y aquí tenemos el de “generic.json”

1
2
3
4
5
6
7
8
{{
try:
   from gluon.serializers import json
   response.write(json(response._vars),escape=False)
   response.headers['Content-Type']='text/json'
except:
   raise HTTP(405,'no json')
}}

Cualquier diccionario puede ser mostrado en HTML, XML y JSON siempre que sólo contenga tipos primitivos python (int, float, string, list, tuple, dictionary). response._vars contiene el diccionario devuelto por la acción.

Si el diccionario contiene otros objetos definidos por el usuarío o específicos de web2py, éstos deben presentarse en una vista personalizada.

Presentando Filas

Si usted necesita presentar un conjunto de Filas tal como las devuelve un select en XML o JSON u otro formato, primero transforme el objeto Rows en una lista de diccionarios usando el método as_list().

Considere, por ejemplo, el siguiente modo:

1
db.define_table('person', Field('name'))

La siguiente acción puede ser presentada en HTML, pero no en XML ni JSON:

1
2
3
def everybody():
    people = db().select(db.person.ALL)
    return dict(people=people)

Mientras la siguiente acción puede ser presentada en XML y JSON:

1
2
3
def everybody():
    people = db().select(db.person.ALL).as_list()
    return dict(people=people)

Formatos personalizados

Si, por ejemplo, se quiere presentar una acción como un pickle Python:

http://127.0.0.1:8000/app/default/count.pickle

solo necesita crear un nuevo archivo vista “default/count.pickle” que contenga:

1
2
3
4
5
{{
import cPickle
response.headers['Content-Type'] = 'application/python.pickle'
response.write(cPickle.dumps(response._vars),escape=False)
}}

Si quiere ser capaz de presentar cualquier acción como un archivo “pickleado”, solo se necesita guardar el anterior archivo con el nombre “generic.pickle”.

No todos los objetos se pueden “picklear”, y no todos los objetos “pickleados” pueden ser “despickelados”. Es seguro continuar con objetos Python primitivos y combinaciones de ellos. Objetos que no contienen referencias a streams de archivos o conexiones de base de datos son generalmente “pickleables”, pero solo pueden ser “des-pickleados” en un ambiente donde las clases de todos los objetos “pickleados” están ya definidas.

RSS

web2py incluye una vista rss genérica “generic.rss” que puede presentar el diccionario devuelto por la acción como una fuente RSS.

Debido a que las fuentes RSS tienen una estructura fija (título, enlace, descripción, elementos, etc.), para que esto trabaje, el diccionario devuelto por la acción debe tener la estructura apropiada:

1
2
3
4
5
{'title'      : ",
 'link'       : ",
 'description': ",
 'created_on' : ",
 'entries'    : []}

y cada entrada en entries debe tener la misma estructura similar:

1
2
3
4
{'title'      : ",
 'link'       : ",
 'description': ",
 'created_on' : "}

Por ejemplo, la siguiente acción puede ser presentada como una fuente RSS:

1
2
3
4
5
6
7
8
9
def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=[
                  dict(title="my feed",
                  link="http://feed.example.com",
                  description="my first feed")
                ])

simplemente visitando el URL:

http://127.0.0.1:8000/app/default/feed.rss

Alternativamente, asumiendo el siguiente modelo:

1
2
3
4
5
db.define_table('rss_entry',
    Field('title'),
    Field('link'),
    Field('created_on','datetime'),
    Field('description'))

la siguiente acción también puede ser presentada como un alimentador RSS:

1
2
3
4
5
def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=db().select(db.rss_entry.ALL).as_list())

El método as_list() del objeto Rows convierte las filas en una lista de diccionarios.

Si se encuentran elementos del diccionario adicionales con nombre clave no listados explícitamente, son ignoran.

Aquí está la vista”generic.rss” provista por web2py:

{{
try:
   from gluon.serializers import rss
   response.write(rss(response._vars),escape=False)
   response.headers['Content-Type']='application/rss+xml'
except:
   raise HTTP(405,'no rss')
}}

Como un ejemplo más de una aplicación RSS, consideramos un agregador RSS que recoje datos de la fuente “slashdot” y retorna una nueva fuente rss web2py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def aggregator():
    import gluon.contrib.feedparser as feedparser
    d = feedparser.parse(
        "http://rss.slashdot.org/Slashdot/slashdot/to")
    return dict(title=d.channel.title,
                link = d.channel.link,
                description = d.channel.description,
                created_on = request.now,
                entries = [
                  dict(title = entry.title,
                  link = entry.link,
                  description = entry.description,
                  created_on = request.now) for entry in d.entries])

Puede accederse en:

http://127.0.0.1:8000/app/default/aggregator.rss

CSV

El formato de los Valores Separados por Coma (CSV for sus iniciales en Inglés) es un protocolo para representar datos en forma de tabla.

Considere el siguiente modelo:

1
2
3
4
db.define_model('animal',
    Field('species'),
    Field('genus'),
    Field('family'))

y la siguiente acción:

1
2
3
def animals():
    animals = db().select(db.animal.ALL)
    return dict(animals=animals)

web2py no provee un “generic.csv”; usted debe definir una vista personalizada “default/animals.csv” que serialize los animales en CSV. Aquí tenemos una posible implementación:

1
2
3
4
5
6
7
{{
import cStringIO
stream=cStringIO.StringIO()
animals.export_to_csv_file(stream)
response.headers['Content-Type']='application/vnd.ms-excel'
response.write(stream.getvalue(), escape=False)
}}

Note que uno también podría definir un archivo “generic.csv”, pero se tendría que especificar el nombre del objeto a ser serializado (“animals” en el ejemplo). Esta es la razón por la cual no proveemos un archivo “generic.csv”.

Llamadas a Procedimientos Remotos (RPC)

web2py provee un mecanismo para convertir cualquier función en un servicio web. El mecanismo descrito aquí difiere del mecanismo descrito antes porque:

  • La función puede tomar argumentos
  • La función puede estar definida en un modelo o módulo en vez de en un controlador
  • Usted puede querer especificar en detalle cual método RPC debe soportarse
  • Impone una convención para nombrado de URL más estricta
  • Es mas inteligente que los métodos previos porque trabaja para un conjunto fijo de protocolos. Por esta misma razón no es expandible tan fácilmente como los otros métodos.

Para usar esta característica:

Primero, usted debe importar y crear instancias de un objeto de servicio (service).

1
2
from gluon.tools import Service
service = Service(globals())

Esto es hecho ya en el archivo modelo “db.py” en la aplicación de andamiaje.

Segundo, debe exponerse el manejador de servicio en el controlador:

1
2
3
def call():
    session.forget()
    return service()

Esto ya lo hace el controlador “default.py” de la aplicación de andamiaje. Remueva session.forget() si tiene pensado usar los cookies de sesión con los servicios.

Tercero, debe decorar las funciones que quiere exponer como servicio. Aquí está una lista de los decoradores soportados actualmente:

@service.run
@service.xml
@service.json
@service.rss
@service.csv
@service.xmlrpc
@service.jsonrpc
@service.amfrpc3('domain')
@service.soap('FunctionName',returns={'result':type},args={'param1':type,})

Como ejemplo, considere la siguiente función decorada:

1
2
3
@service.run
def concat(a,b):
    return a+b

Esta función puede ser definida en un modelo o en el controlador donde la acción call es definida. Esta función ahora puede ser llamada remotamente de dos formas:

http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world http://127.0.0.1:8000/app/default/call/run/concat/hello/world

En ambos casos la solicitud http devuelve:

1.helloworld

Si se usa el decorador @service.xml, la función puede ser llamada por medio de:

http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world http://127.0.0.1:8000/app/default/call/xml/concat/hello/world

y la salida retorna como XML:

1
2
3
<document>
   <result>helloworld</result>
</document>

Puede serializar la salida de la función aún si está es un objeto DAL Rows. En este caso, de hecho, llamará as_list() de manera automática.

Si el decorador @service.json es usado, la función puede ser llamada por medio de:

http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world http://127.0.0.1:8000/app/default/call/json/concat/hello/world

y la salida devuelta como JSON.

Si el decorador @service.csv es usado, el manejador del servicio requiere, como valor de retorno, un objeto iterable de objetos iterables, tal como una lista de listas. He aquí un ejemplo:

1
2
3
@service.csv
def table1(a,b):
    return [[a,b],[1,2]]

Este servicio puede ser llamado visitando uno de los siguientes URLs:

http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world http://127.0.0.1:8000/app/default/call/csv/table1/hello/world

y devuelve:

1. hello,world
2. 1,2

El decorador @service.rss espera un valor de retorno en el mismo formato que el de la vista “generic.rss” discutida en la sección anterior.

Se permiten múltiples decoradores para cada función.

Hasta ahora, todo lo discutido en esta sección es simplemente una alternativa al método descrito en la sección previa. El verdadero poder del objeto servicio viene con XMLRPC, JSONRPC y AMFRPC, como se discute a continuación.

XMLRPC

Considere el siguiente código, por ejemplo, en el controlador “default.py”:

1
2
3
4
5
6
@service.xmlrpc
def add(a,b):
    return a+b
@service.xmlrpc
def div(a,b):
    return a/b

Ahora en una cónsola de python se puede hacer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> from xmlrpclib import ServerProxy
>>> server = ServerProxy(
       'http://127.0.0.1:8000/app/default/call/xmlrpc')
>>> print server.add(3,4)
7
>>> print server.add('hello','world')
'helloworld'
>>> print server.div(12,4)
3
>>> print server.div(1,0)
ZeroDivisionError: integer division or modulo by zero

El módulo Python xmlrpclib provee un cliente para el protocolo XMLRPC. web2py actúa como servidor.

El cliente se conecta al servidor por medio del ServerProxy y puede llamar remotamente a funciones decoradas en el servidor. La data (a,b) es pasada a la función(es), no a través de las variables GET/POST, sino adecuadamente codificadas en el cuerpo de la solicitud usando el protocolo XMLPRC, y por lo tanto lleva en si misma información del tipo (entero o cadena u otro). Lo mismo es cierto para el(los) valor(es) de retorno. Más aún, cualquier excepción que se produzca en el servidor se propaga de regreso al cliente.

Hay librerías XMLRPC para muchos lenguajes de programación (incluyendo C, C++, Java, C#, Ruby, y Perl), y pueden interoperar entre ellos. Éste es uno de los mejores métodos para crear aplicaciones que “hablan” entre sí independientemente del lenguaje de programación.

El cliente XMLRPC también puede ser implementado dentro de una acción web2py, de tal forma que una acción pueda hablar con otra aplicación web2py (incluso dentro de la misma instalación) usando XMLRPC. En este caso hay que cuidarse de los bloqueos de sesión. Si una acción llama a una función a través de XMLRPC en la misma aplicación, el que llama debe desbloquear la sesión antes de la llamada:

1
2
session.forget()
session._unlock(response)

JSONRPC y Pyjamas

JSONRPC es muy similar a XMLRPC, pero usa el protocolo basado en JSON en vez de XML para codificar los datos. Aquí, como un ejmplo de aplicación, discutiremos su uso con Pyjamas. Pyjamas es un puerto Python de las Herramientas de Web Google - Google Web Toolkit – (escrito originalmente en Java). Pyjamas permite escribir una aplicación cliente en Python. Pyjamas traduce este código a JavaScript. web2py sirve el JavaScript y se comunica con él a través de solicitudes AJAX que se originan del cliente y son activadas por acciones del usuario.

Aquí se describe como hacer que Pyjamas trabaje con web2py. No requiere librerías adicionales distintas a web2py y Pyjamas.

Ahora vamos a construir una simple aplicación “todo” con un cliente Pyjamas (totalmente en JavaScript) que habla con el servidor exclusivamente a través de JSONRPC.

Primero, cree una nueva aplicación llamada “todo”.

Segundo, en “models/db.py”, agregue el siguiente código:

db=SQLDB('sqlite://storage.sqlite')
    db.define_table('todo', Field('task'))
    from gluon.tools import Service     # import rpc services
    service = Service(globals())

Tercero, en “controllers/default.py”, agregue el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def index():
    redirect(URL('todoApp'))
    @service.jsonrpc
    def getTasks():
        todos = db(db.todo.id>0).select()
        return [(todo.task,todo.id) for todo in todos]
    @service.jsonrpc
    def addTask(taskFromJson):
        db.todo.insert(task= taskFromJson)
        return getTasks()
    @service.jsonrpc
    def deleteTask (idFromJson):
        del db.todo[idFromJson]
        return getTasks()
    def call():
        session.forget()
        return service()
    def todoApp():
        return dict()

El propósito de cada función debe ser obvio.

Cuarto, en “views/default/todoApp.html”, agregue el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
  <head>
    <meta name="pygwt:module"
     content="{{=URL('static','output/todoapp')}}" />
    <title>
      simple todo application
    </title>
  </head>
  <body bgcolor="white">
    <h1>
      simple todo application
    </h1>
    <i>
      type a new task to insert in db,
      click on existing task to delete it
    </i>
    <script language="javascript"
     src="{{=URL('static','output/pygwt.js')}}">
    </script>
  </body>
</html>

Esta vista sólo ejecuta el código Pyjamas en “static/output/todoapp” – código que aún no se ha creado.

Quinto, en “static/TodoApp.py” (Note que es TodoApp, no todoApp!), ingrese el siguiente código del cliente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.TextBox import TextBox
import pyjamas.ui.KeyboardListener
from pyjamas.ui.ListBox import ListBox
from pyjamas.ui.HTML import HTML
from pyjamas.JSONService import JSONProxy
class TodoApp:
    def onModuleLoad(self):
        self.remote = DataService()
        panel = VerticalPanel()
        self.todoTextBox = TextBox()
        self.todoTextBox.addKeyboardListener(self)
        self.todoList = ListBox()
        self.todoList.setVisibleItemCount(7)
        self.todoList.setWidth("200px")
        self.todoList.addClickListener(self)
        self.Status = Label("")
        panel.add(Label("Add New Todo:"))
        panel.add(self.todoTextBox)
        panel.add(Label("Click to Remove:"))
        panel.add(self.todoList)
        panel.add(self.Status)
        self.remote.getTasks(self)
        RootPanel().add(panel)
    def onKeyUp(self, sender, keyCode, modifiers):
        pass
    def onKeyDown(self, sender, keyCode, modifiers):
        pass
    def onKeyPress(self, sender, keyCode, modifiers):
        """
        This function handles the onKeyPress event, and will add the
        item in the text box to the list when the user presses the
        enter key. In the future, this method will also handle the
        auto complete feature.
        """
        if keyCode == KeyboardListener.KEY_ENTER and \
           sender == self.todoTextBox:
            id = self.remote.addTask(sender.getText(),self)
            sender.setText("")
            if id<0:
                RootPanel().add(HTML("Server Error or Invalid Response"))
    def onClick(self, sender):
        id = self.remote.deleteTask(
                sender.getValue(sender.getSelectedIndex()),self)
        if id<0:
            RootPanel().add(
                HTML("Server Error or Invalid Response"))
    def onRemoteResponse(self, response, request_info):
        self.todoList.clear()
        for task in response:
            self.todoList.addItem(task[0])
            self.todoList.setValue(self.todoList.getItemCount()-1,
                                   task[1])
    def onRemoteError(self, code, message, request_info):
        self.Status.setText("Server Error or Invalid Response: " \
                            + "ERROR " + code + " - " + message)
class DataService(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "../../default/call/jsonrpc",
                           ["getTasks", "addTask","deleteTask"])
if __name__ == '__main__':
    app = TodoApp()
    app.onModuleLoad()

Sexto, ejecute Pyjamas antes de servir la aplicación:

cd /path/to/todo/static/
python /python/pyjamas-0.5p1/bin/pyjsbuild TodoApp.py

Esto traducirá el código Python a JavaScript de tal forma que pueda ser ejecutado en el navegador.

Para acceder esta aplicación, visite el URL:

http://127.0.0.1:8000/todo/default/todoApp

Esta subsección fué creada por Chris Prinos con ayuda de Luke Kenneth Casson Leighton (creadores de Pyjamas), actualizada por Alexei Vinidiktov. Ha sido probada con Pyjamas 0.5p1. El ejemplo se inspiró en esta página Django en ref. 74.

AMFRPC

AMFRPC es el protocolo de Llamada a Procedimiento Remoto usado por clientes Flash para comunicarse con un servidor. web2py soporta AMFRPC, pero requiere que ejecute web2py desde la fuente y que instale previamente la librería PyAMF. Ésta puede ser instalada desde la cónsola de Linux o Windows escribiendo:

1.easy_install pyamf

(por favor consultar la documentación PyAMF para mas detalles).

En esta subsección asumimos que ya está familiarizado con la programación ActionScript.

Crearemos un servicio simple que toma dos valores numéricos, los suma, y luego devuelve la suma. Llamaremos “pyamf_test” a nuestra aplicación web2py, y llamaremos al servicio addNumbers.

Primero, usando Adobe Flash (cualquier versión comenzando con la MX 2004), cree la aplicación del cliente Flash client creando un nuevo archivo Flash FLA. En el primer cuadro del archivo, agregue estas líneas:

import mx.remoting.Service;
import mx.rpc.RelayResponder;
import mx.rpc.FaultEvent;
import mx.rpc.ResultEvent;
import mx.remoting.PendingCall;
var val1 = 23;
var val2 = 86;
service = new Service(
    "http://127.0.0.1:8000/pyamf_test/default/call/amfrpc3",
    null, "mydomain", null, null);
var pc:PendingCall = service.addNumbers(val1, val2);
pc.responder = new RelayResponder(this, "onResult", "onFault");
function onResult(re:ResultEvent):Void {
    trace("Result : " + re.result);
    txt_result.text = re.result;
}
function onFault(fault:FaultEvent):Void {
    trace("Fault: " + fault.fault.faultstring);
}
stop();

Este código permite al ciente Flash conectarse a un servicio que corresponde a una función llamada “addNumbers” en el archivo “/pyamf_test/default/gateway”. Usted debe también importar las clases remotas de ActionScript versión 2 MX para habilitar el Remoto en Flash. Agregue la ruta a estas clases a la configuración de rutas de clases en el IDE de Adobe Flash, o simplemente coloque la carpeta “mx” al lado del nuevo archivo creado.

Note los argumentos del constructor del Servicio. El primer argumento es la URL correspondiente al servicio que queremos crear. El tercer argumento es el dominio del servicio. Escogimos llamar a este dominio “mydomain”.

Segundo, cree un campo de texto dinámico llamado “txt_result” y coloquelo en la plataforma.

Tercero, usted necesitá configurar una pasarela web2py que pueda comunicarse con el cliente Flash definido arriba.

Proceda creando una nueva aplicación web2py llamada pyamf_test that que hospedará el nuevo servicio y la pasarela AMF para el cliente flash. Edite el controlador “default.py” y asegúrese que contiene

1
2
3
4
@service.amfrpc3('mydomain')
def addNumbers(val1, val2):
    return val1 + val2
def call(): return service()

Cuarto, compile y exporte/publique el cliente flash SWF como pyamf_test.swf, coloque los archivos “pyamf_test.amf”, “pyamf_test.html”, “AC_RunActiveContent.js”, y “crossdomain.xml” en la carpeta “static” del dispositivo recién creado que está hospedando la pasarela, “pyamf_test”.

Ahora puede probar el cliente visitando:

http://127.0.0.1:8000/pyamf_test/static/pyamf_test.html

La pasarela se llama en segundo plano cuando el cliente se conecta a addNumbers.

Si está usando AMF0 en vez de AMF3 puede también usar el decorador:

1.@service.amfrpc

en vez de:

1.@service.amfrpc3('mydomain')

En este caso también necesitará cambiar la URL del servicio a:

http://127.0.0.1:8000/pyamf_test/default/call/amfrpc

SOAP

web2py incluye un cliente y servidor SOAP creado por Mariano Reingart. Este puede ser usado casi igual que XML-RPC:

Considere el siguiente código, por ejemplo, en el controlador “default.py”:

1
2
3
@service.soap('MyAdd',returns={'result':int},args={'a':int,'b':int,})
def add(a,b):
    return a+b

Ahora en una cónsola de python puede hacer:

1
2
3
4
>>> from gluon.contrib.pysimplesoap.client import SoapClient
>>> client = SoapClient(wsdl="http://localhost:8000/app/default/call/soap?WSDL")
>>> print client.MyAdd(a=1,b=2)
{'result': 3}

Para obtener codificación apropiada cuando se devuelven valores de texto, especifique las cadenas como u’proper utf8 text’.

Usted puede obtener el WSDL para el servicio en:

http://127.0.0.1:8000/app/default/call/soap?WSDL

Y puede obtener documentación para cualquiera de los métodos expuestos:

http://127.0.0.1:8000/app/default/call/soap

API de Bajo Nivel y Otras Recetas

simplejson

web2py incluye gluon.contrib.simplejson, desarrollado por Bob Ippolito. Este módulo proprociona el codificador-decodificador Python-JSON más estándarizado.

SimpleJSON consta de dos funciones:

  • gluon.contrib.simplesjson.dumps(a) codifica un objeto “a” Python a JSON.
  • gluon.contrib.simplejson.loads(b) decodifica un objeto JavaScript “b” en un objeto Python.

Los tipos de objetos que pueden ser serializados incluyen tipos primitivos, listas y diccionarios. Objetos compuestos pueden ser serializados con la excepción de las clases definidas por los usuarios.

Aquí tenemos una acción de muestra (por ejemplo en el controlador “default.py”) que serializa la lista Python que contiene días de la semana usando esta API de bajo nivel:

1
2
3
4
5
def weekdays():
    names=['Sunday','Monday','Tuesday','Wednesday',
           'Thursday','Friday','Saturday']
    import gluon.contrib.simplejson
    return gluon.contrib.simplejson.dumps(names)

Debajo está una página HTML de muestra que envía una solicitud Ajax a la acción de arriba, recibe el mensaje JSON y guarda la lista en la variable JavaScript correspondiente:

1
2
3
4
5
{{extend 'layout.html'}}
<script>
$.getJSON('/application/default/weekdays',
          function(data){ alert(data); });
</script>

El código usa la función jQuery $.getJSON, la cual realiza la llamada Ajax y, al responder, guarda los nombres de los días de la semana en una variable local JavaScript data y pasa la variable a la función que devuelve la llamada. En el ejemplo la función que devuelve la llamada simplemente alerta al visitante que los datos han sido recibidos.

PyRTF

Otra necesidad común de los sitios web es la de generar documentos de texto leíbles por Word. La forma más simple de hacer esto es usando el formato de documentos Formato de Texto Mejorado (RTF por sus siglas en Inglés). Este formato fué inventado por Microsoft y a partir de allí se ha convertido en un estándar.

web2py incluye gluon.contrib.pyrtf, desarrollado por Simon Cusack y revisado por Grant Edwards. Este módulo permite generar documentos RTF de manera programática incluyendo texto formateado coloreado además de fotos.

En el siguiente ejemplo se crearán dos instancias de clases básicas RTF, Document y Section, anexe la última a la primera e inserte cualquier texto de prueba en la segunda:

1
2
3
4
5
6
7
8
9
def makertf():
    import gluon.contrib.pyrtf as q
    doc=q.Document()
    section=q.Section()
    doc.Sections.append(section)
    section.append('Section Title')
    section.append('web2py is great. '*100)
    response.headers['Content-Type']='text/rtf'
    return q.dumps(doc)

Al final el Document es serializado por q.dumps(doc). Note que antes de devolver un documento RTF es necesario especificar el tipo de contenido en el encabezado, de lo contrario el navegador no sabrá como manejar el archivo.

Dependiendo de la configuración, el navegador puede preguntar si se quiere guardar este archivo o si se desea abrirlo usando un editor de texto.

ReportLab y PDF

web2py también puede generar documentos PDF, con una librería adicional llamada “ReportLab”73.

Si está ejecutando web2py desde la fuente, es suficiente con tener instalado ReportLab. Si está ejecutando la distribución binaria de Windows, necesita descomprimir ReportLab en la carpeta “web2py/”. Si está ejecutando la distribución binaria Mac, necesita descomprimir ReportLab en la carpeta:

1.web2py.app/Contents/Resources/

A partir de aquí se asume que ReportLab está instalado y que web2py puede hallarlo. Crearemos una acción simple llamada “get_me_a_pdf” que genera un documento PDF.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from reportlab.platypus import *
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib import colors
from uuid import uuid4
from cgi import escape
import os
def get_me_a_pdf():
    title = "This The Doc Title"
    heading = "First Paragraph"
    text = 'bla '* 10000
    styles = getSampleStyleSheet()
    tmpfilename=os.path.join(request.folder,'private',str(uuid4()))
    doc = SimpleDocTemplate(tmpfilename)
    story = []
    story.append(Paragraph(escape(title),styles["Title"]))
    story.append(Paragraph(escape(heading),styles["Heading2"]))
    story.append(Paragraph(escape(text),styles["Normal"]))
    story.append(Spacer(1,2*inch))
    doc.build(story)
    data = open(tmpfilename,"rb").read()
    os.unlink(tmpfilename)
    response.headers['Content-Type']='application/pdf'
    return data

Observe como generamos el PDF en un archivo temporal único, tmpfilename, leemos el PDF generado del archivo, y luego borramos el archivo.

Para mas información acerca de la API ReportLab, use como referencia la documentación de ReportLab. Le recomendamos fuertemente el uso de la API Platypus de ReportLab, tal como Paragraph, Spacer, etc.

Servicios y Autentificación

En el capítulo anterior se discutió el uso de los siguientes decoradores:

@auth.requires_login()
@auth.requires_membership(...)
@auth.requires_permission(...)

Para acciones normales (no decoradas como servicios), estos decoradores se pueden usar aún si la salida se presenta en un formato diferente a HTML.

Para funciones definidas como servicios y decoradas usando el decorador @service..., los decoradores @auth... no deben usarse. Los dos tipos de decoradores no pueden mezclarse. Si va a realizar autenticación, son las acciones call las que necesitan ser decoradas:

1
2
@auth.requires_login()
def call(): return service()

Note que también es posible declarar una instancia de múltiples objetos de servicio, registrar las mismas funciones diferentes con ellos, y exponer alguna de ellas con autenticación y otras no:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public_services=Service(globals())
private_services=Service(globals())
@public_service.jsonrpc
@private_service.jsonrpc
def f(): return 'public'
@private_service.jsonrpc
def g(): return 'private'
def public_call(): return public_service()
@auth.requires_login()
def private_call(): return private_service()

Esto asume que el que llama está pasando credenciales en el encabezado del HTTP (una cookie de sesión válida o usando autenticación básica, como se discutió en la sección anterior). El cliente debe soportarlo; no todos los clientes lo hacen.

Contenidos

Tema anterior

Control de Acceso

Próximo tema

Recetas Ajax

Envíe sus comentarios o correcciones a comunidad@latinuxpress.com

Patrocinado por