xmlrpc.client — Biblioteca de cliente para XML-RPC

Propósito:Biblioteca del lado del cliente para la comunicación XML-RPC.

XML-RPC es un protocolo ligero de llamada a procedimiento remoto, construido en sobre HTTP y XML. El módulo xmlrpclib permite que un programa de Python se comunique con un servidor XML-RPC escrito en cualquier idioma.

Todos los ejemplos en esta sección usan el servidor definido en xmlrpc_server.py, disponible en la distribución de código fuente y es incluido aquí para referencia.

xmlrpc_server.py
from xmlrpc.server import SimpleXMLRPCServer
from xmlrpc.client import Binary
import datetime


class ExampleService:

    def ping(self):
        """Simple function to respond when called
        to demonstrate connectivity.
        """
        return True

    def now(self):
        """Returns the server current date and time."""
        return datetime.datetime.now()

    def show_type(self, arg):
        """Illustrates how types are passed in and out of
        server methods.

        Accepts one argument of any type.

        Returns a tuple with string representation of the value,
        the name of the type, and the value itself.

        """
        return (str(arg), str(type(arg)), arg)

    def raises_exception(self, msg):
        "Always raises a RuntimeError with the message passed in"
        raise RuntimeError(msg)

    def send_back_binary(self, bin):
        """Accepts single Binary argument, and unpacks and
        repacks it to return it."""
        data = bin.data
        print('send_back_binary({!r})'.format(data))
        response = Binary(data)
        return response


if __name__ == '__main__':
    server = SimpleXMLRPCServer(('localhost', 9000),
                                logRequests=True,
                                allow_none=True)
    server.register_introspection_functions()
    server.register_multicall_functions()

    server.register_instance(ExampleService())

    try:
        print('Use Control-C to exit')
        server.serve_forever()
    except KeyboardInterrupt:
        print('Exiting')

Conectar a un servidor

La forma más sencilla de conectar un cliente a un servidor es instanciar un objeto ServerProxy, dándole el URI del servidor. Por ejemplo, el servidor de demostración se ejecuta en el puerto 9000 de localhost.

xmlrpc_ServerProxy.py
import xmlrpc.client

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

En este caso, el método ping() del servicio no toma argumentos y devuelve un solo valor booleano.

$ python3 xmlrpc_ServerProxy.py

Ping: True

Otras opciones están disponibles para soportar transporte alternativo. Ambos HTTP y HTTPS son compatibles de fábrica, ambos con autenticación básica. Para implementar un nuevo canal de comunicación, solo se necesita una nueva clase de transporte. Podría ser un ejercicio interesante, por ejemplo, implementar XML-RPC sobre SMTP.

xmlrpc_ServerProxy_verbose.py
import xmlrpc.client

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

La opción verbose da información de depuración útil para resolver errores de comunicación.

$ python3 xmlrpc_ServerProxy_verbose.py

send: b'POST /RPC2 HTTP/1.1\r\nHost: localhost:9000\r\n
Accept-Encoding: gzip\r\nContent-Type: text/xml\r\n
User-Agent: Python-xmlrpc/3.5\r\nContent-Length: 98\r\n\r\n'
send: b"<?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 header: Date header: Content-type header:
Content-length body: b"<?xml version='1.0'?>\n<methodResponse>\n
<params>\n<param>\n<value><boolean>1</boolean></value>\n</param>
\n</params>\n</methodResponse>\n"
Ping: True

La codificación predeterminada se puede cambiar desde UTF-8 si un sistema alternativo es necesario.

xmlrpc_ServerProxy_encoding.py
import xmlrpc.client

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

El servidor detecta automáticamente la codificación correcta.

$ python3 xmlrpc_ServerProxy_encoding.py

Ping: True

La opción allow_none controla si el valor None de Python se traduce automáticamente a un valor nulo o si se produce un error.

xmlrpc_ServerProxy_allow_none.py
import xmlrpc.client

server = xmlrpc.client.ServerProxy('http://localhost:9000',
                                   allow_none=False)
try:
    server.show_type(None)
except TypeError as err:
    print('ERROR:', err)

server = xmlrpc.client.ServerProxy('http://localhost:9000',
                                   allow_none=True)
print('Allowed:', server.show_type(None))

El error se genera localmente si el cliente no permite None, pero también se puede generar desde el servidor si no está configurado para permitir None.

$ python3 xmlrpc_ServerProxy_allow_none.py

ERROR: cannot marshal None unless allow_none is enabled
Allowed: ['None', "<class 'NoneType'>", None]

Tipos de datos

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

xmlrpc_types.py
import xmlrpc.client
import datetime

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

data = [
    ('boolean', True),
    ('integer', 1),
    ('float', 2.5),
    ('string', 'some text'),
    ('datetime', datetime.datetime.now()),
    ('array', ['a', 'list']),
    ('array', ('a', 'tuple')),
    ('structure', {'a': 'dictionary'}),
]

for t, v in data:
    as_string, type_name, value = server.show_type(v)
    print('{:<12}: {}'.format(t, as_string))
    print('{:12}  {}'.format('', type_name))
    print('{:12}  {}'.format('', value))

Los tipos simples son

$ python3 xmlrpc_types.py

boolean     : True
              <class 'bool'>
              True
integer     : 1
              <class 'int'>
              1
float       : 2.5
              <class 'float'>
              2.5
string      : some text
              <class 'str'>
              some text
datetime    : 20160618T19:31:47
              <class 'xmlrpc.client.DateTime'>
              20160618T19:31:47
array       : ['a', 'list']
              <class 'list'>
              ['a', 'list']
array       : ['a', 'tuple']
              <class 'list'>
              ['a', 'tuple']
structure   : {'a': 'dictionary'}
              <class 'dict'>
              {'a': 'dictionary'}

Los tipos soportados se pueden anidar para crear valores de complejidad arbitraria.

xmlrpc_types_nested.py
import xmlrpc.client
import datetime
import pprint

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

data = {
    'boolean': True,
    'integer': 1,
    'floating-point number': 2.5,
    'string': 'some text',
    'datetime': datetime.datetime.now(),
    'array1': ['a', 'list'],
    'array2': ('a', 'tuple'),
    'structure': {'a': 'dictionary'},
}
arg = []
for i in range(3):
    d = {}
    d.update(data)
    d['integer'] = i
    arg.append(d)

print('Before:')
pprint.pprint(arg, width=40)

print('\nAfter:')
pprint.pprint(server.show_type(arg)[-1], width=40)

Este programa pasa una lista de diccionarios que contiene todos los tipos admitidos al servidor de muestra, que devuelve los datos. Tuplas se convierten a listas y las instancias datetime se convierten a los objetos DateTime, pero por lo demás los datos no se modifican.

$ python3 xmlrpc_types_nested.py

Before:
[{'array': ('a', 'tuple'),
  'boolean': True,
  'datetime': datetime.datetime(2016, 6, 18, 19, 27, 30, 45333),
  'floating-point number': 2.5,
  'integer': 0,
  'string': 'some text',
  'structure': {'a': 'dictionary'}},
 {'array': ('a', 'tuple'),
  'boolean': True,
  'datetime': datetime.datetime(2016, 6, 18, 19, 27, 30, 45333),
  'floating-point number': 2.5,
  'integer': 1,
  'string': 'some text',
  'structure': {'a': 'dictionary'}},
 {'array': ('a', 'tuple'),
  'boolean': True,
  'datetime': datetime.datetime(2016, 6, 18, 19, 27, 30, 45333),
  'floating-point number': 2.5,
  'integer': 2,
  'string': 'some text',
  'structure': {'a': 'dictionary'}}]

After:
[{'array': ['a', 'tuple'],
  'boolean': True,
  'datetime': <DateTime '20160618T19:27:30' at 0x101ecfac8>,
  'floating-point number': 2.5,
  'integer': 0,
  'string': 'some text',
  'structure': {'a': 'dictionary'}},
 {'array': ['a', 'tuple'],
  'boolean': True,
  'datetime': <DateTime '20160618T19:27:30' at 0x101ecfcc0>,
  'floating-point number': 2.5,
  'integer': 1,
  'string': 'some text',
  'structure': {'a': 'dictionary'}},
 {'array': ['a', 'tuple'],
  'boolean': True,
  'datetime': <DateTime '20160618T19:27:30' at 0x101ecfe10>,
  'floating-point number': 2.5,
  'integer': 2,
  'string': 'some text',
  'structure': {'a': 'dictionary'}}]

XML-RPC admite fechas como tipo nativo, y xmlrpclib puede usar una de las dos clases para representar los valores de fecha en el proxy saliente o cuando se reciban del servidor.

xmlrpc_ServerProxy_use_datetime.py
import xmlrpc.client

server = xmlrpc.client.ServerProxy('http://localhost:9000',
                                   use_datetime=True)
now = server.now()
print('With:', now, type(now), now.__class__.__name__)

server = xmlrpc.client.ServerProxy('http://localhost:9000',
                                   use_datetime=False)
now = server.now()
print('Without:', now, type(now), now.__class__.__name__)

Por defecto se usa una versión interna de DateTime, pero la opción use_datetime activa el soporte para usar las clases en el módulo datetime.

$ python3 source/xmlrpc.client/xmlrpc_ServerProxy_use_datetime.py

With: 2016-06-18 19:18:31 <class 'datetime.datetime'> datetime
Without: 20160618T19:18:31 <class 'xmlrpc.client.DateTime'> DateTime

Pasar objetos

Las instancias de las clases de Python se tratan como estructuras y se pasan como diccionario, con los atributos del objeto como valores en el diccionario.

xmlrpc_types_object.py
import xmlrpc.client
import pprint


class MyObj:

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __repr__(self):
        return 'MyObj({!r}, {!r})'.format(self.a, self.b)


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

o = MyObj(1, 'b goes here')
print('o  :', o)
pprint.pprint(server.show_type(o))

o2 = MyObj(2, o)
print('\no2 :', o2)
pprint.pprint(server.show_type(o2))

Cuando el valor se devuelve al cliente desde el servidor, el resultado es un diccionario en el cliente, ya que no hay nada codificado en los valores para indicar al servidor (o cliente) que debe ser instanciado como parte de una clase.

$ python3 xmlrpc_types_object.py

o  : MyObj(1, 'b goes here')
["{'b': 'b goes here', 'a': 1}", "<class 'dict'>",
{'a': 1, 'b': 'b goes here'}]

o2 : MyObj(2, MyObj(1, 'b goes here'))
["{'b': {'b': 'b goes here', 'a': 1}, 'a': 2}",
 "<class 'dict'>",
 {'a': 2, 'b': {'a': 1, 'b': 'b goes here'}}]

Datos binarios

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

xmlrpc_Binary.py
import xmlrpc.client
import xml.parsers.expat

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

s = b'This is a string with control characters\x00'
print('Local string:', s)

data = xmlrpc.client.Binary(s)
response = server.send_back_binary(data)
print('As binary:', response.data)

try:
    print('As string:', server.show_type(s))
except xml.parsers.expat.ExpatError as err:
    print('\nERROR:', err)

Si la cadena que contiene un byte NULO se pasa a show_type(), se genera una excepción en el analizador XML mientras procesa la respuesta.

$ python3 xmlrpc_Binary.py

Local string: b'This is a string with control characters\x00'
As binary: b'This is a string with control characters\x00'

ERROR: not well-formed (invalid token): line 6, column 55

Los objetos Binary también pueden usarse para enviar objetos usando pickle. Los problemas de seguridad normales relacionados con el envío de lo que resulta ser código ejecutable a través del cable se aplican aquí (es decir, no hagas esto a menos que el canal de comunicación sea seguro).


import xmlrpc.client
import pickle
import pprint


class MyObj:

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __repr__(self):
        return 'MyObj({!r}, {!r})'.format(self.a, self.b)


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

o = MyObj(1, 'b goes here')
print('Local:', id(o))
print(o)

print('\nAs object:')
pprint.pprint(server.show_type(o))

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

o2 = pickle.loads(r.data)
print('\nFrom pickle:', id(o2))
pprint.pprint(o2)

El atributo de datos de la instancia Binary contiene el versión serializada del objeto, por lo que debe ser de serializado antes de que pueda ser usado. Eso resulta en un objeto diferente (con un nuevo valor de id).

$ python3 xmlrpc_Binary_pickle.py

Local: 4327262304
MyObj(1, 'b goes here')

As object:
["{'a': 1, 'b': 'b goes here'}", "<class 'dict'>",
{'a': 1, 'b': 'b goes here'}]

From pickle: 4327262472
MyObj(1, 'b goes here')

Manejo de excepciones

Dado que el servidor XML-RPC puede estar escrito en cualquier idioma, las clases de excepción no pueden ser transmitidas directamente. En cambio, las excepciones elevadas en el servidor se convierte en objetos Fault y se elevan como excepciones a nivel local en el cliente.

xmlrpc_exception.py
import xmlrpc.client

server = xmlrpc.client.ServerProxy('http://localhost:9000')
try:
    server.raises_exception('A message')
except Exception as err:
    print('Fault code:', err.faultCode)
    print('Message   :', err.faultString)

El mensaje de error original se guarda en el atributo faultString, y faultCode se establece en un número de error XML-RPC.

$ python3 xmlrpc_exception.py

Fault code: 1
Message   : <class 'RuntimeError'>:A message

Combinar llamadas en un solo mensaje

Multicall es una extensión del protocolo XML-RPC que permite enviar más de una llamada al mismo tiempo, con las respuestas recolectadas y devueltas a la persona que llama.

xmlrpc_MultiCall.py
import xmlrpc.client

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

multicall = xmlrpc.client.MultiCall(server)
multicall.ping()
multicall.show_type(1)
multicall.show_type('string')

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

Para usar una instancia MultiCall, invoca los métodos en ella como con un ServerProxy, luego llama al objeto sin argumentos para en realidad ejecutar las funciones remotas. El valor de retorno es un iterador que produce los resultados de todas las llamadas.

$ python3 xmlrpc_MultiCall.py

0 True
1 ['1', "<class 'int'>", 1]
2 ['string', "<class 'str'>", 'string']

Si una de las llamadas provoca un Fault, se genera la excepción cuando el resultado se produce desde el iterador y no hay más resultados disponibles.

xmlrpc_MultiCall_exception.py
import xmlrpc.client

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

multicall = xmlrpc.client.MultiCall(server)
multicall.ping()
multicall.show_type(1)
multicall.raises_exception('Next to last call stops execution')
multicall.show_type('string')

try:
    for i, r in enumerate(multicall()):
        print(i, r)
except xmlrpc.client.Fault as err:
    print('ERROR:', err)

Ya que la tercera respuesta, desde raises_exception(), genera una excepción, la respuesta de show_type() no es accesible.

$ python3 xmlrpc_MultiCall_exception.py

0 True
1 ['1', "<class 'int'>", 1]
ERROR: <Fault 1: "<class 'RuntimeError'>:Next to last call stops execution">

Ver también