xmlrpc.server — Un servidor XML-RPC

Propósito:Implementa un servidor XML-RPC.

El módulo xmlrpc.server contiene clases para crear servidores multi plataforma, independientes del lenguaje utilizando el protocolo XML-RPC. Existen bibliotecas cliente para muchos otros idiomas además de Python, haciendo de XML-RPC una opción fácil para construir servicios de estilo RPC.

Nota

Todos los ejemplos proporcionados aquí incluyen un módulo de cliente también para interactuar con el servidor de demostración. Para ejecutar los ejemplos, utiliza dos ventanas shell separadas, una para el servidor y otra para el cliente.

Un servidor simple

Este simple ejemplo de servidor expone una única función que toma el nombre de un directorio y devuelve los contenidos. El primer paso es crear la instancia SimpleXMLRPCServer y decirle dónde escuchar las solicitudes entrantes (puerto 9000 “localhost” en este caso). Entonces una función se define como parte del servicio, y es registrada para que el servidor sepa cómo llamarla. El paso final es poner el servidor en un bucle infinito recibiendo y respondiendo peticiones.

Advertencia

Esta implementación tiene obvias implicaciones de seguridad. No la corras en un servidor en el Internet abierto o en cualquier entorno donde la seguridad pueda ser un problema.

xmlrpc_function.py
from xmlrpc.server import SimpleXMLRPCServer
import logging
import os

# Set up logging
logging.basicConfig(level=logging.INFO)

server = SimpleXMLRPCServer(
    ('localhost', 9000),
    logRequests=True,
)


# Expose a function
def list_contents(dir_name):
    logging.info('list_contents(%s)', dir_name)
    return os.listdir(dir_name)


server.register_function(list_contents)

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

Se puede acceder al servidor en la URL http://localhost:9000 usando xmlrpc.client. Este código de cliente ilustra cómo llamar al servicio list_contents() desde Python.

xmlrpc_function_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print(proxy.list_contents('/tmp'))

El ServerProxy está conectado al servidor usando su URL base, y luego los métodos se llaman directamente en el proxy. Cada método invocado en el proxy se traduce en una solicitud al servidor. Los argumentos se formatean utilizando XML y luego se envían al servidor en una mensaje POST. El servidor desempaqueta el XML y determina qué función llamar según el nombre del método invocado desde el cliente. Los argumentos se pasan a la función, y el valor de retorno es traducido de nuevo a XML para ser devuelto al cliente.

Al iniciar el servidor se obtiene la siguiente salida.

$ python3 xmlrpc_function.py

Use Control-C to exit

Ejecutar el cliente en una segunda ventana muestra el contenido de la Directorio /tmp.

$ python3 xmlrpc_function_client.py

['com.apple.launchd.aoGXonn8nV', 'com.apple.launchd.ilryIaQugf',
'example.db.db',
'KSOutOfProcessFetcher.501.ppfIhqX0vjaTSb8AJYobDV7Cu68=',
'pymotw_import_example.shelve.db']

Una vez finalizada la solicitud, la salida de registro aparece en la ventana del servidor.

$ python3 xmlrpc_function.py

Use Control-C to exit
INFO:root:list_contents(/tmp)
127.0.0.1 - - [18/Jun/2016 19:54:54] "POST /RPC2 HTTP/1.1" 200 -

La primera línea de salida es de la llamada logging.info() dentro de list_contents(). La segunda línea es desde el servidor que registra la solicitud porque logRequests es True.

Nombres alternativos de interfaz de programación

A veces, los nombres de funciones utilizados dentro de un módulo o biblioteca no son los nombres que se deben utilizar en la interfaz de programación externa. Los nombres pueden cambiar porque se carga una implementación específica de la plataforma, la interfaz de programación del servicio se construye dinámicamente en base a un archivo de configuración, o funciones reales puede ser reemplazado con stubs para el testeo. Para registrar una función con un nombre alternativo, pasa el nombre como segundo argumento para register_function().

xmlrpc_alternate_name.py
from xmlrpc.server import SimpleXMLRPCServer
import os

server = SimpleXMLRPCServer(('localhost', 9000))


def list_contents(dir_name):
    "Expose a function with an alternate name"
    return os.listdir(dir_name)


server.register_function(list_contents, 'dir')

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

El cliente ahora debe usar el nombre dir() en lugar de list_contents().

xmlrpc_alternate_name_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print('dir():', proxy.dir('/tmp'))
try:
    print('\nlist_contents():', proxy.list_contents('/tmp'))
except xmlrpc.client.Fault as err:
    print('\nERROR:', err)

Al llamar a list_contents() se produce un error, ya que el servidor ya no tiene un controlador registrado con ese nombre.

$ python3 xmlrpc_alternate_name_client.py

dir(): ['com.apple.launchd.aoGXonn8nV',
'com.apple.launchd.ilryIaQugf', 'example.db.db',
'KSOutOfProcessFetcher.501.ppfIhqX0vjaTSb8AJYobDV7Cu68=',
'pymotw_import_example.shelve.db']

ERROR: <Fault 1: '<class \'Exception\'>:method "list_contents"
is not supported'>

Nombres de interfaz de programación con puntos

Funciones individuales se pueden registrar con nombres que no son normalmente legales como los identificadores de Python. Por ejemplo, un punto (.) se puede incluir en los nombres para separar el espacio de nombres en el servicio. El siguiente ejemplo extiende el servicio «directory» para agregar las llamadas «create» y «remove». Todas las funciones se registran utilizando el prefijo «dir» para que el mismo servidor pueda proporcionar otros servicios usando un prefijo diferente. Otra diferencia en este ejemplo es que algunas de las funciones devuelven None, por lo que el servidor debe ser avisado para que traduzca los valores de None a un valor nulo.

xmlrpc_dotted_name.py
from xmlrpc.server import SimpleXMLRPCServer
import os

server = SimpleXMLRPCServer(('localhost', 9000), allow_none=True)

server.register_function(os.listdir, 'dir.list')
server.register_function(os.mkdir, 'dir.create')
server.register_function(os.rmdir, 'dir.remove')

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

Para llamar a las funciones de servicio en el cliente, simplemente refiérete a ellas con el nombre con puntos.

xmlrpc_dotted_name_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print('BEFORE       :', 'EXAMPLE' in proxy.dir.list('/tmp'))
print('CREATE       :', proxy.dir.create('/tmp/EXAMPLE'))
print('SHOULD EXIST :', 'EXAMPLE' in proxy.dir.list('/tmp'))
print('REMOVE       :', proxy.dir.remove('/tmp/EXAMPLE'))
print('AFTER        :', 'EXAMPLE' in proxy.dir.list('/tmp'))

Suponiendo que no hay un archivo /tmp/EXAMPLE en el sistema actual, la salida para el script de cliente de ejemplo es la siguiente.

$ python3 xmlrpc_dotted_name_client.py

BEFORE       : False
CREATE       : None
SHOULD EXIST : True
REMOVE       : None
AFTER        : False

Nombres arbitrarios de interfaz de programación

Otra característica interesante es la posibilidad de registrar funciones con nombres que de otra manera son nombres inválidos de atributos de objetos de Python. Este servicio por ejemplo registra una función con el nombre «multiply args «.

xmlrpc_arbitrary_name.py
from xmlrpc.server import SimpleXMLRPCServer

server = SimpleXMLRPCServer(('localhost', 9000))


def my_function(a, b):
    return a * b


server.register_function(my_function, 'multiply args')

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

Dado que el nombre registrado contiene un espacio, la notación de puntos no puede ser utilizada para acceder directamente desde el proxy. Usar getattr() funciona, sin embargo.

xmlrpc_arbitrary_name_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print(getattr(proxy, 'multiply args')(5, 5))

Sin embargo, evita crear servicios con nombres como este. Este ejemplo se proporciona, no necesariamente porque es una buena idea, sino porque existen servicios con nombres arbitrarios, y nuevos programas pueden necesitar poder llamarlos.

$ python3 xmlrpc_arbitrary_name_client.py

25

Exponer métodos de objetos

Las secciones anteriores hablaban sobre técnicas para establecer interfaces de programación usando buenas convenciones de nombres y espacios de nombres. Otra forma de incorporar espacios de nombres en una interfaz de programación es usar instancias de clases y exponer sus métodos. El primer ejemplo puede ser recreado usando un instancia con un solo método.

xmlrpc_instance.py
from xmlrpc.server import SimpleXMLRPCServer
import os
import inspect

server = SimpleXMLRPCServer(
    ('localhost', 9000),
    logRequests=True,
)


class DirectoryService:
    def list(self, dir_name):
        return os.listdir(dir_name)


server.register_instance(DirectoryService())

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

Un cliente puede llamar al método directamente.

xmlrpc_instance_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print(proxy.list('/tmp'))

La salida muestra los contenidos del directorio.

$ python3 xmlrpc_instance_client.py

['com.apple.launchd.aoGXonn8nV', 'com.apple.launchd.ilryIaQugf',
'example.db.db',
'KSOutOfProcessFetcher.501.ppfIhqX0vjaTSb8AJYobDV7Cu68=',
'pymotw_import_example.shelve.db']

Sin embargo, el prefijo «dir» para el servicio se ha perdido. Puede ser restaurado mediante la definición de una clase para configurar un árbol de servicios que puede ser invocado por los clientes.

xmlrpc_instance_dotted_names.py
from xmlrpc.server import SimpleXMLRPCServer
import os
import inspect

server = SimpleXMLRPCServer(
    ('localhost', 9000),
    logRequests=True,
)


class ServiceRoot:
    pass


class DirectoryService:

    def list(self, dir_name):
        return os.listdir(dir_name)


root = ServiceRoot()
root.dir = DirectoryService()

server.register_instance(root, allow_dotted_names=True)

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

Al registrar la instancia de ServiceRoot con allow_dotted_names habilitado, el servidor tiene permiso para recorrer el árbol de objetos cuando llega una solicitud para encontrar el método nombrado usando getattr().

xmlrpc_instance_dotted_names_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print(proxy.dir.list('/tmp'))

La salida de dir.list() es la misma que con la anteriores implementaciones.

$ python3 xmlrpc_instance_dotted_names_client.py

['com.apple.launchd.aoGXonn8nV', 'com.apple.launchd.ilryIaQugf',
'example.db.db',
'KSOutOfProcessFetcher.501.ppfIhqX0vjaTSb8AJYobDV7Cu68=',
'pymotw_import_example.shelve.db']

Despacho de llamadas

Por defecto, register_instance() encuentra todos los atributos que se pueden llamar de la instancia con nombres que no empiecen con un guión bajo («_») y los registra con sus nombres. Para tener más cuidado con los métodos expuestos, se puede utilizar lógica de despacho personalizada.

xmlrpc_instance_with_prefix.py
from xmlrpc.server import SimpleXMLRPCServer
import os
import inspect

server = SimpleXMLRPCServer(
    ('localhost', 9000),
    logRequests=True,
)


def expose(f):
    "Decorator to set exposed flag on a function."
    f.exposed = True
    return f


def is_exposed(f):
    "Test whether another function should be publicly exposed."
    return getattr(f, 'exposed', False)


class MyService:
    PREFIX = 'prefix'

    def _dispatch(self, method, params):
        # Remove our prefix from the method name
        if not method.startswith(self.PREFIX + '.'):
            raise Exception(
                'method "{}" is not supported'.format(method)
            )

        method_name = method.partition('.')[2]
        func = getattr(self, method_name)
        if not is_exposed(func):
            raise Exception(
                'method "{}" is not supported'.format(method)
            )

        return func(*params)

    @expose
    def public(self):
        return 'This is public'

    def private(self):
        return 'This is private'


server.register_instance(MyService())

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

El método public() de MyService está marcado como expuesto al servicio XML-RPC mientras que private() no lo está. El método _dispatch() se invoca cuando el cliente intenta acceder a una función que forma parte de MyService. Primero impone el uso de un prefijo («prefix» en este caso, pero cualquier cadena puede ser utilizada). Entonces se requiere que la función tenga un atributo llamado exposed con un valor verdadero. La bandera expuesta se establece en una función utilizando un decorador para mayor comodidad. El siguiente ejemplo incluye algunas llamadas de cliente de muestra.

xmlrpc_instance_with_prefix_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
print('public():', proxy.prefix.public())
try:
    print('private():', proxy.prefix.private())
except Exception as err:
    print('\nERROR:', err)
try:
    print('public() without prefix:', proxy.public())
except Exception as err:
    print('\nERROR:', err)

La salida resultante, con los mensajes de error esperados atrapados y informados, sigue.

$ python3 xmlrpc_instance_with_prefix_client.py

public(): This is public

ERROR: <Fault 1: '<class \'Exception\'>:method "prefix.private" is
not supported'>

ERROR: <Fault 1: '<class \'Exception\'>:method "public" is not
supported'>

Hay varias otras formas de anular el mecanismo de despacho, incluyendo la subclasificación directamente desde SimpleXMLRPCServer. Remítete a las cadenas de documentación en el módulo para más detalles.

Interfaz de programación de introspección

Al igual que con muchos servicios de red, es posible consultar un servidor XML-RPC para preguntarle qué métodos soporta y aprender a usarlos. SimpleXMLRPCServer incluye un conjunto de métodos públicos para realizar esta introspección. Por defecto están desactivados, pero pueden habilitarse con register_introspection_functions(). Soporte para system.listMethods() y system.methodHelp() puede agregarse a un servicio definiendo _listMethods() y _methodHelp() en la clase de servicio.

xmlrpc_introspection.py
from xmlrpc.server import (SimpleXMLRPCServer,
                           list_public_methods)
import os
import inspect

server = SimpleXMLRPCServer(
    ('localhost', 9000),
    logRequests=True,
)
server.register_introspection_functions()


class DirectoryService:

    def _listMethods(self):
        return list_public_methods(self)

    def _methodHelp(self, method):
        f = getattr(self, method)
        return inspect.getdoc(f)

    def list(self, dir_name):
        """list(dir_name) => [<filenames>]

        Returns a list containing the contents of
        the named directory.

        """
        return os.listdir(dir_name)


server.register_instance(DirectoryService())

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

En este caso, la función de conveniencia list_public_methods() escanea una instancia para devolver los nombres de atributos que se pueden llamar que no comience con guión bajo (_). Redefine _listMethods() para aplicar las reglas que desees. Del mismo modo, para este ejemplo básico _methodHelp() devuelve la cadena de documentación de la función, pero podría ser escrito para construir una cadena de ayuda de otra fuente.

Este cliente consulta el servidor e informa sobre todos los métodos que se pueden llamar públicamente.

xmlrpc_introspection_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy('http://localhost:9000')
for method_name in proxy.system.listMethods():
    print('=' * 60)
    print(method_name)
    print('-' * 60)
    print(proxy.system.methodHelp(method_name))
    print()

Los métodos del sistema están incluidos en los resultados.

$ python3 xmlrpc_introspection_client.py

============================================================
list
------------------------------------------------------------
list(dir_name) => [<filenames>]

Returns a list containing the contents of
the named directory.

============================================================
system.listMethods
------------------------------------------------------------
system.listMethods() => ['add', 'subtract', 'multiple']

Returns a list of the methods supported by the server.

============================================================
system.methodHelp
------------------------------------------------------------
system.methodHelp('add') => "Adds two integers together"

Returns a string containing documentation for the specified method.

============================================================
system.methodSignature
------------------------------------------------------------
system.methodSignature('add') => [double, int, int]

Returns a list describing the signature of the method. In the
above example, the add method takes two integers as arguments
and returns a double result.

This server does NOT support system.methodSignature.

Ver también