PyMOTW: xmlrpclib

24 Julio 2008

Traducción de PyMOTW: xmlrpclib el módulo xmlrpclib de la columna semanal de Doug Hellmann.


Comentarios

Como continuación del artículo de la semana pasada sobre SimpleXMLRPCServer (original), esta semana cubre la librería para clientes xmlrpclib.

Módulo: xmlrpclib
Propósito: Librería de comunicación para clientes XML-RPC
Versión de Python: 2.2 y posterior

Descripción:

La semana pasada vimos la librería para crear un servidor XML-RPC. El módulo xmlrpclib te permite comunicar desde Python con cualquier servidor XML-RPC escrito en cualquier lenguaje. Todos los ejemplos a continuación usan el servidor definido en xmlrpclib_server.py, disponible en el código distribuido y que se repite aquí para cualquier referencia:

Conectando a un servidor:

La manera más simple de conectar un cliente al servidor es instanciar un objeto ServerProxy, proporcionándole el URI del servidor. Por ejemplo, el servidor de demostración corre en el puerto 9000 de localhost:

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000')
print 'Ping:', server.ping()

En este caso, el método ping() del servicio no necesita argumentos y devuelve un sólo valor binario.

$ python xmlrpclib_ServerProxy.py
Ping: True

Otras opciones están disponibles para soportar transporte alternativo. Ambos, HTTP y HTTPS, son soportados directamente, como lo es autenticación básica. Sólo necesitarás proveer una clase de transporte si tu canal de comunicación no es uno de los tipos soportados. Sería un ejercicio interesante, por ejemplo, implementar XML-RPC sobre SMTP. Nada extremadamente útil, pero interesante.

La opción verbose te da información útil para identificar dónde pueden estar ocurriendo errores de comunicación.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000', verbose=True)
print 'Ping:', server.ping()

Ping: connect: (localhost, 9000)
send: 'POST /RPC2 HTTP/1.0\r\nHost: localhost:9000\r\nUser-Agent: xmlrpclib.py/1.0.1 (by www.pythonware.com)\r\nContent-Type: text/xml\r\nContent-Length: 98\r\n\r\n'
send: "<?xml version='1.0'?>\n<methodCall>\n<methodName>ping</methodName>\n<params>\n</params>\n</methodCall>\n"
reply: 'HTTP/1.0 200 OK\r\n'
header: Server: BaseHTTP/0.3 Python/2.5.1
header: Date: Fri, 25 Jul 2008 00:38:28 GMT
header: Content-type: text/xml
header: Content-length: 129
body: "<?xml version='1.0'?>\n<methodResponse>\n<params>\n<param>\n<value><boolean>1</boolean></value>\n</param>\n</params>\n</methodResponse>\n"
True

Puedes cambiar la codificación por defecto de UTF-8 si necesitas para usar un sistema alternativo.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000', encoding='ISO-8859-1')
print 'Ping:', server.ping()

El servidor debería detectar automáticamente la codificación correcta.

$ python xmlrpclib_ServerProxy_encoding.py
Ping: True

La opción allow_none controla si el valor None de Python es traducido automáticamente al valor nil o si ocasiona un error.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000', allow_none=True)
print 'Permitido:', server.show_type(None)

server = xmlrpclib.ServerProxy('http://localhost:9000', allow_none=False)
print 'No permitido:', server.show_type(None)

Nota que el error es elevado localmente si el cliente no permite None, pero puede ser también elevado desde el servidor si no está configurado para permitir None/

$ python xmlrpclib_ServerProxy_allow_none.py
Permitido: ['None', "<type 'NoneType'>", None]
No permitido:
Traceback (most recent call last):
  File "xmlrpclib_ServerProxy_allow_none.py", line 17, in <module>
    print 'No permitido:', server.show_type(None)
  File "/usr/lib/python2.5/xmlrpclib.py", line 1147, in __call__
    return self.__send(self.__name, args)
  File "/usr/lib/python2.5/xmlrpclib.py", line 1431, in __request
    allow_none=self.__allow_none)
  File "/usr/lib/python2.5/xmlrpclib.py", line 1080, in dumps
    data = m.dumps(params)
  File "/usr/lib/python2.5/xmlrpclib.py", line 623, in dumps
    dump(v, write)
  File "/usr/lib/python2.5/xmlrpclib.py", line 635, in __dump
    f(self, value, write)
  File "/usr/lib/python2.5/xmlrpclib.py", line 639, in dump_nil
    raise TypeError, "cannot marshal None unless allow_none is enabled"
TypeError: cannot marshal None unless allow_none is enabled

La opción use_datetime te permite pasarle datetime.datetime y objetos relacionados al proxy o recibirlos del servidor. Si use_datetime es False, la clase interna DateTime es usada para representar fechas.

Tipos de datos:

El protocolo XML-RPC reconoce un conjunto limitado de tipos de datos comunes. Los tipos pueden ser pasados como argumentos o valores de retorno y combinados para crear estructuras de datos más complejas.

import xmlrpclib
import datetime

server = xmlrpclib.ServerProxy('http://localhost:9000')

for t, v in [ ('binario', True), 
              ('numero entero', 1),
              ('numero de punto flotante', 2.5),
              ('cadena', 'un texto'), 
              ('fecha y hora', datetime.datetime.now()),
              ('array', ['a', 'lista']),
              ('array', ('a', 'tupla')),
              ('estructura', {'a': 'diccionario'}),
            ]:
    print '%-25s:' % t, server.show_type(v)

Los tipos (de datos) simples:

$ python xmlrpclib_types.py
binario                  : ['True', "<type 'bool'>", True]
numero entero            : ['1', "<type 'int'>", 1]
numero de punto flotante : ['2.5', "<type 'float'>", 2.5]
cadena                   : ['un texto', "<type 'str'>", 'un texto']
fecha y hora             : ['20080724T20:47:59', "<type 'instance'>", <DateTime '20080724T20:47:59' at b7d605ec>]
array                    : ["['a', 'lista']", "<type 'list'>", ['a', 'lista']]
array                    : ["['a', 'tupla']", "<type 'list'>", ['a', 'tupla']]
estructura               : ["{'a': 'diccionario'}", "<type 'dict'>", {'a': 'diccionario'}]

Por supuesto, ellos pueden ser combinados para creas valores de complejidad arbitraria.

import xmlrpclib
import datetime
import pprint

server = xmlrpclib.ServerProxy('http://localhost:9000')

data = { 'binario': True, 
         'numero entero': 1,
         'numero de punto flotante': 2.5,
         'cadena': 'un texto',
         'fecha y hora': datetime.datetime.now(),
         'array': ['a', 'list'],
         'array': ('a', 'tuple'),
         'estructura': {'a': 'dictionary'},
         }
arg = []
for i in range(3):
    d = {}
    d.update(data)
    d['numero entero'] = i
    arg.append(d)

print 'Antes:'
pprint.pprint(arg)

print
print 'Después:'
pprint.pprint(server.show_type(arg)[-1])

$ python xmlrpclib_types_nested.py
Antes:
[{'array': ('a', 'tuple'),
  'binario': True,
  'cadena': 'un texto',
  'estructura': {'a': 'dictionary'},
  'fecha y hora': datetime.datetime(2008, 7, 24, 20, 52, 33, 437064),
  'numero de punto flotante': 2.5,
  'numero entero': 0},
 {'array': ('a', 'tuple'),
  'binario': True,
  'cadena': 'un texto',
  'estructura': {'a': 'dictionary'},
  'fecha y hora': datetime.datetime(2008, 7, 24, 20, 52, 33, 437064),
  'numero de punto flotante': 2.5,
  'numero entero': 1},
 {'array': ('a', 'tuple'),
  'binario': True,
  'cadena': 'un texto',
  'estructura': {'a': 'dictionary'},
  'fecha y hora': datetime.datetime(2008, 7, 24, 20, 52, 33, 437064),
  'numero de punto flotante': 2.5,
  'numero entero': 2}]

Después:
[{'array': ['a', 'tuple'],
  'binario': True,
  'cadena': 'un texto',
  'estructura': {'a': 'dictionary'},
  'fecha y hora': <DateTime '20080724T20:52:33' at b7cbdeac>,
  'numero de punto flotante': 2.5,
  'numero entero': 0},
 {'array': ['a', 'tuple'],
  'binario': True,
  'cadena': 'un texto',
  'estructura': {'a': 'dictionary'},
  'fecha y hora': <DateTime '20080724T20:52:33' at b7a5ad2c>,
  'numero de punto flotante': 2.5,
  'numero entero': 1},
 {'array': ['a', 'tuple'],
  'binario': True,
  'cadena': 'un texto',
  'estructura': {'a': 'dictionary'},
  'fecha y hora': <DateTime '20080724T20:52:33' at b7a5af2c>,
  'numero de punto flotante': 2.5,
  'numero entero': 2}]

Pasando objetos:

Instancias de clases Python son tratadas como estructuras y pasadas como un diccionario, con atributos del objeto como valores en el diccionario.

import xmlrpclib

class MyObj:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __repr__(self):
        return 'MyObj(%s, %s)' % (repr(self.a), repr(self.b))

server = xmlrpclib.ServerProxy('http://localhost:9000')

o = MyObj(1, 'aqui viene b')
print 'o=', o
print server.show_type(o)

o2 = MyObj(2, o)
print 'o2=', o2
print server.show_type(o2)

Iterando el valor ofrece un diccionario en el cliente, ya que no hay nada codificado en los valores que la diga al servidor (o al cliente) que debería ser instanciado como parte de una clase.

$ python xmlrpclib_types_object.py
o= MyObj(1, 'aqui viene b')
["{'a': 1, 'b': 'aqui viene b'}", "<type 'dict'>", {'a': 1, 'b': 'aqui viene b'}]
o2= MyObj(2, MyObj(1, 'aqui viene b'))
["{'a': 2, 'b': {'a': 1, 'b': 'aqui viene b'}}", "<type 'dict'>", {'a': 2, 'b': {'a': 1, 'b': 'aqui viene b'}}]

Datos binarios:

Todos los valores pasados al servidor están codificados y escapados automáticamente. Sin embargo, algunos tipos de datos pueden contener caracteres que no son válidos en XML. Por ejemplo datos binarios de imágenes pueden incluir valores de byte en el rango de control de ASCI de 0 a 31. Si necesitas pasar datos binarios, lo mejor es utilizar la clase Binary para codificarlos para el transporte.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000')

s = 'Ésta es una cadena con caracteres de control' + '\0'
print 'Cadena local:', s

data = xmlrpclib.Binary(s)
print 'Como binario:', server.send_back_binary(data)

print 'Como cadena:', server.show_type(s)

Si pasamos la cadena que contenga un byte NULL a show_type(), se eleva una excepción en el parser de XML.

$ python xmlrpclib_Binary.py
Cadena local: Ésta es una cadena con caracteres de control
Como binario: Ésta es una cadena con caracteres de control
Como cadena:
Traceback (most recent call last):
  File "xmlrpclib_Binary.py", line 21, in <module>
    print 'Como cadena:', server.show_type(s)
  File "/usr/lib/python2.5/xmlrpclib.py", line 1147, in __call__
    return self.__send(self.__name, args)
  File "/usr/lib/python2.5/xmlrpclib.py", line 1437, in __request
    verbose=self.__verbose
  File "/usr/lib/python2.5/xmlrpclib.py", line 1201, in request
    return self._parse_response(h.getfile(), sock)
  File "/usr/lib/python2.5/xmlrpclib.py", line 1340, in _parse_response
    return u.close()
  File "/usr/lib/python2.5/xmlrpclib.py", line 787, in close
    raise Fault(**self._stack[0])
xmlrpclib.Fault: <Fault 1: "<class 'xml.parsers.expat.ExpatError'>:not well-formed (invalid token): line 6, column 59">

También se puede usar datos binarios para enviar objetos usando pickle. Las cuestiones normales de seguridad relacionadas al envío de lo que viene a ser un ejecutable a través de la red son aplicables aquí (es decir, no lo hagas a menos que estés seguro que tu canal de comunicación es seguro).

import xmlrpclib
import cPickle as pickle

class MyObj:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __repr__(self):
        return 'MyObj(%s, %s)' % (repr(self.a), repr(self.b))

server = xmlrpclib.ServerProxy('http://localhost:9000')

o = MyObj(1, 'aqui viene b')
print 'Local:', o, id(o)

print 'Como objeto:', server.show_type(o)

p = pickle.dumps(o)
b = xmlrpclib.Binary(p)
r = server.send_back_binary(b)

o2 = pickle.loads(r.data)
print 'Desde pickle:', o2, id(o2)

Recuerda, el atributo data de la instancia Binary contiene la versión pickle del objeto, osea que tiene que ser convertida antes de poder ser usada. Ésto resulta en un objeto diferente (con un nuevo valor id).

$ python xmlrpclib_Binary_pickle.py
Local: MyObj(1, 'aqui viene b') 3083488236
Como objeto: ["{'a': 1, 'b': 'aqui viene b'}", "<type 'dict'>", {'a': 1, 'b': 'aqui viene b'}]
Desde pickle: MyObj(1, 'aqui viene b') 3083632108

Manejando excepciones:

Dado que el servidor XML-RPC puede estar escrito en cualquier lenguaje, las clases de excepciones no pueden ser traducidas directamente. En su lugar, las excepciones elevadas en el servidor son convertidas en objetos Fault y elevadas como excepciones localmente.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000')
try:
    server.raises_exception('Un mensaje')
except Exception, err:
    print 'Código de error:', err.faultCode
    print 'Mensaje        :', err.faultString

$ python xmlrpclib_exception.py
Código de error: 1
Mensaje        : <type 'exceptions.RuntimeError'>:Un mensaje

MultiCall:

MultiCall es una extensión al protocolo XML-RPC que permite enviar más de una invocación a la vez, que recolecta las respuestas (los resultados) y los devuelve al invocador. La clase MultiCall fue añadida a xmlrpclib en Python 2.4. Para usar una instancia MultiCall, invoca los métodos en ella como con un ServerProxy, luego invoca al objeto sin argumentos. El resultado es un iterador con los resultados.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000')

multicall = xmlrpclib.MultiCall(server)
multicall.ping()
multicall.show_type(1)
multicall.show_type('cadena')

for i, r in enumerate(multicall()):
    print i, r

$ python xmlrpclib_MultiCall.py
0 True
1 ['1', "<type 'int'>", 1]
2 ['cadena', "<type 'str'>", 'cadena']

Si una de las invocaciones ocasiona un error (Fault) o eleva de otra manera una excepción, la excepción es elevada cuando el resultado es producido por el iterador y no hay más resultados disponibles.

import xmlrpclib

server = xmlrpclib.ServerProxy('http://localhost:9000')

multicall = xmlrpclib.MultiCall(server)
multicall.ping()
multicall.show_type(1)
multicall.raises_exception('Penultima invocacion termina la ejecucion')
multicall.show_type('cadena')

for i, r in enumerate(multicall()):
    print i, r

$ python xmlrpclib_MultiCall_exception.py
0 True
1 ['1', "<type 'int'>", 1]
Traceback (most recent call last):
  File "xmlrpclib_MultiCall_exception.py", line 21, in <module>
    for i, r in enumerate(multicall()):
  File "/usr/lib/python2.5/xmlrpclib.py", line 949, in __getitem__
    raise Fault(item['faultCode'], item['faultString'])
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.RuntimeError'>:Penultima invocacion termina la ejecucion">

Referencias:

PyMOTW: SimpleXMLRPCServer (original), PyMOTW: SimpleXMLRPCServer (traducción)
XML-RPC How To
XML-RPC Extensions
Python Module of the Week Home
Descarga el código

Copyright 2008 Doug Hellmann


blog comments powered by Disqus

Categorías