PyMOTW: SimpleXMLRPCServer

9 Julio 2008

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


Comentarios

El módulo SimpleXMLRPCServer facilita el uso de invocaciones remotas (remote procedure calls) en Python que funcionan con muchos otros lenguajes también.

Módulo: SimpleXMLRPCServer
Propósito: Implementa un servidor XML-RPC.
Versión de Python: 2.2 y posterior

Descripción

El módulo SimpleXMLRPCServer contiene clases para crear tu propio servidor multi plataforma, independientemente del lenguaje usando en protocolo XML-RPC. Existen librerías para clientes en muchos otros lenguajes, haciendo XML-RPC una elección fácil para crear servicios al estilo RPC.

Nota: Todos los ejemplos aquí incluyen un cliente también para interactuar con el servidor de demostración. Si quieres descargar el código y ejecutar los ejemplos, querrás usar dos ventanas separadas de linea de comando, una para el servidor y otra para el cliente.

Un servidor simple

Este ejemplo de servidor simple expone una sola función que toma el nombre de un directorio y devuelve el contenido (obviamente un problema de seguridad, pero útil para propósitos demostrativos). El primer paso es crear la instancia de SimpleXMLRPCServer y decirle dónde esperar solicitudes ('localhost' puerto 9000 en este caso). Entonces definimos una función que será parte del servicio, y registramos las función para que el servidor sepa cómo invocarla. El paso final es poner al servidor en un lazo infinito recibiendo y respondiendo las solicitudes.

from SimpleXMLRPCServer import SimpleXMLRPCServer
import logging
import os

# Prepara registro
logging.basicConfig(level=logging.DEBUG)

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

# Expone una función
def list_contents(dir_name):
    logging.debug('list_contents(%s)', dir_name)
    return os.listdir(dir_name)
server.register_function(list_contents)

try:
    print 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

El servidor es accesible en el URL http://localhost:9000 usando xmlrpclib. Este código de cliente ilustra como se puede invocar el servicio list_contents() desde Python

import xmlrpclib

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

Nota que simplemente conectamos el ServerProxy al servidor usando el URL base, y que invocamos métodos directamente en el proxy. Cada método que es invocado en el proxy es traducido en un pedido al servidor. Los argumentos son formateados usando XML, y enviados al servidor con el método POST. El servidor desempaqueta el XML y averigua qué función invocar en base en el nombre del método invocado desde el cliente. Los argumentos se pasan a a función, y el valor de retorno es traducido de vuelta a XML para ser devuelto al cliente.

Iniciando el servidor da:

$ python SimpleXMLRPCServer_function.py 
Usa Control-C para salir

Ejecutando el cliente en una segunda ventana muestra el contenido de mi directorio /tmp:

$ python SimpleXMLRPCServer_function_client.py 
['ssh-EnjpDF9256', '.ICE-unix', 'virtual-ers.acWNsN', 'seahorse-RyC0FS',
'seahorse-fsFKhW', 'keyring-xPmGkq', 'v369262', 'orbit-ers', '.X0-lock', 
'ssh-AjVAcI6655', 'mapping-ers', 'gconfd-ers', 'Tracker-ers.9363', 'purpleDU18DU',
'v387578', '.esd', '.X11-unix', 'purple3B00DU', 'plugtmp-1', '.exchange-ers',
'sqlbEv95W', 'plugtmp', 'Tracker-ers.6771']

y después de que el pedido a terminado, verás el registro en la ventana del servidor:

$ python SimpleXMLRPCServer_function.py 
Usa Control-C para salir
DEBUG:root:list_contents(/tmp)
localhost - - [09/Jul/2008 22:59:45] "POST /RPC2 HTTP/1.0" 200 -

La primera línea de la salida es de la invocación de logging.debug() dentro de list_contents(). La segunda línea es del servidor registrando el pedido porque puse logRequests=True.

Nombre alternativos

A veces los nombres de funciones que usas dentro de tus módulos o librerías no son los nombres que quieres usar en tu interfaz de programación externa. Puedes necesitar cargar una implementación específica para una plataforma, crear la interfaz de programación del servicio en base a un archivo de configuración, o reemplazar funciones verdaderas con fragmentos de prueba. Si quieres registrar una función con un nombre alternativo, pasa el nombre como el segundo argumento a register_function(), así:

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os

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

# Exponer una función con un nombre alternativo
def list_contents(dir_name):
    return os.listdir(dir_name)
server.register_function(list_contents, 'dir')

try:
    print 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

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

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print 'dir():', proxy.dir('/tmp')
print 'list_contents():', proxy.list_contents('/tmp')

Invocar list_contents() resulta en un error, ya que el servidor ya no tiene un controlador registrado con ese nombre.

$ python SimpleXMLRPCServer_alternate_name_client.py
dir(): ['ssh-EnjpDF9256', '.ICE-unix', 'virtual-ers.acWNsN', 'seahorse-RyC0FS',
'seahorse-fsFKhW', 'keyring-xPmGkq', 'v369262', 'orbit-ers', '.X0-lock',
'ssh-AjVAcI6655', 'mapping-ers', 'gconfd-ers', 'Tracker-ers.9363', 'purpleDU18DU',
'v387578', '.esd', '.X11-unix', 'purple3B00DU', 'plugtmp-1', '.exchange-ers',
'sqlbEv95W', 'plugtmp', 'Tracker-ers.6771']
list_contents():
Traceback (most recent call last):
  File "SimpleXMLRPCServer_alternate_name_client.py", line 15, in <module>
    print 'list_contents():', proxy.list_contents('/tmp')
  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: '<type \'exceptions.Exception\'>:method "list_contents" is not supported'>

Nombres con puntos

Funciones individuales pueden ser registradas con nombre que no son normalmente legales para identificadores Python. Por ejemplo, puedes incluir '.' en tus nombres para separar el espacio de nombres en el servicio. Este ejemplo extiende nuestro servicio "directory" para aumentar invocaciones "create" y "remove". Todas las funciones son registradas usando el prefijo "dir." para que el mismo servidor pueda prever otros servicios usando un prefijo distinto. Otra diferencia en este ejemplo es que algunas de las funciones devuelven None, entonces tenemos que decirle al servidor que traduzca los valores None en un valor nil (ver XML-RPC Extensions).

from SimpleXMLRPCServer 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 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'saliendo'

Para invocar las funciones del servicio en el cliente, simplemente refiérete a éllas con el nombre con puntos, así:

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print 'ANTES           :', 'EJEMPLO' in proxy.dir.list('/tmp')
print 'CREAR           :', proxy.dir.create('/tmp/EJEMPLO')
print 'DEBERÍA EXISTIR :', 'EJEMPLO' in proxy.dir.list('/tmp')
print 'REMOVER         :', proxy.dir.remove('/tmp/EJEMPLO')
print 'DEPUÉS          :', 'EJEMPLO' in proxy.dir.list('/tmp')

y (asumiendo que no tienes /tmp/EJEMPLO en tu sistema) el resultado del cliente ejemplo es el siguiente:

$ python SimpleXMLRPCServer_dotted_name_client.py
ANTES           : False
CREAR           : None
DEBERÍA EXISTIR : True
REMOVER         : None
DESUES          : False

Nombres arbitrarios

Otra característica menos útil pero potencialmente interesante es la habilidad de registrar funciones con nombres que son de otra manera nombre inválidos de atributos. Este servicio ejemplo registra una función con el nombre "multiplica los argumentos".

from SimpleXMLRPCServer import SimpleXMLRPCServer

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

def my_function(a, b):
    return a * b
server.register_function(my_function, 'multiplica los argumentos')

try:
    print 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

Ya que el nombre registrado contiene un espacio, no podemos usar la notación con puntos para acceder directamente desde el proxy. Podemos sin embargo, usar getattr().

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print getattr(proxy, 'multiplica los argumentos')(5, 5)

Este ejemplo es proporcionado no necesariamente porque es una buena idea, pero porque puedes que te encuentres servicios existentes con nombre arbitrarios y que necesites poder invocarlos.

$ python SimpleXMLRPCServer_arbitrary_name_client.py
25

Exponiendo métodos de objetos

Hemos hablado de establecer interfaces de programación usando buenas convenciones para poner nombres. Otra manera de lograr éso es usar instancias de clases y exponer sus métodos. Podemos recrear el primer ejemplo usando una instancia con un sólo método.

from SimpleXMLRPCServer 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 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

Un cliente puede invocar el método directamente:

import xmlrpclib

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

y recibir un resultado similar:

$ python SimpleXMLRPCServer_instance_client.py
['ssh-EnjpDF9256', '.ICE-unix', 'virtual-ers.acWNsN', 'seahorse-RyC0FS',
'seahorse-fsFKhW', 'keyring-xPmGkq', 'v369262', 'orbit-ers', '.X0-lock',
'ssh-AjVAcI6655', 'mapping-ers', 'gconfd-ers', 'Tracker-ers.9363', 'purpleDU18DU',
'v387578', '.esd', '.X11-unix', 'purple3B00DU', 'plugtmp-1', '.exchange-ers',
'sqlbEv95W', 'plugtmp', 'Tracker-ers.6771']

Aunque hemos perdido el prefijo 'dir.' para el servicio, entonces definamos una clase que nos permita crear un árbol de servicio que puede ser invocado desde los clientes:

from SimpleXMLRPCServer 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 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

Al registrar la instancia de ServiceRoot con allow_dotted_names=True, le damos permiso al servidor para atravesar el árbol de objetos cuando una solicitud viene a buscar el método nombrado usando getattr()

import xmlrpclib

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

$ python SimpleXMLRPCServer_instance_dotted_names_client.py
['ssh-EnjpDF9256', '.ICE-unix', 'virtual-ers.acWNsN', 'seahorse-RyC0FS',
'seahorse-fsFKhW', 'keyring-xPmGkq', 'v369262', 'orbit-ers', '.X0-lock',
'ssh-AjVAcI6655', 'mapping-ers', 'gconfd-ers', 'Tracker-ers.9363', 'purpleDU18DU',
'v387578', '.esd', '.X11-unix', 'purple3B00DU', 'plugtmp-1', '.exchange-ers',
'sqlbEv95W', 'plugtmp', 'Tracker-ers.6771']

Despachando invocaciones tú mismo

Por defecto, register_instance() encuentra todos los atributos que pueden ser invocados de la instancia con nombre que no comiencen con '_' y los registra con sus nombres. Si quieres ser más cuidadoso con los métodos expuestos, puedes proporcionar tu propia lógica de despacho. Por ejemplo:

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os
import inspect

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

def expose(f):
    "Decorador para poner la bandera de expuesta en una función."
    f.exposed = True
    return f

def is_exposed(f):
    "Prueba si otra función debería ser expuesta públicamente."
    return getattr(f, 'exposed', False)

class MyService:
    PREFIX = 'prefix'

    def _dispatch(self, method, params):
        # Remueve nuestro prefijo del nombre del método
        if not method.startswith(self.PREFIX + '.'):
            raise Exception('metodo "%s" no es soportado' % method)

        method_name = method.partition('.')[2]
        func = getattr(self, method_name)
        if not is_exposed(func):
            raise Exception('metodo "%s" no es soportado' % method)

        return func(*params)

    @expose
    def public(self):
        return 'Éste es público'

    def private(self):
        return 'Éste es privado'

server.register_instance(MyService())

try:
    print 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

El método public() de MyService está marcado como expuesto al servicio XML-RPC mientras que private() no lo es. El método _dispatch() es invocado cuando el cliente intenta acceder una función que es parte de MyService. Primero hace cumplir el uso de un prefijo ("prefix." en este caso, pero puedes usar cualquier cosa en realidad). Luego requiere que la función tenga un atributo llamado "exposed" con un valor positivo. La bandera exposed se establece en una función usando un decorador por comodidad.

Aquí hay unas invocaciones de muestra desde clientes:

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print 'public():', proxy.prefix.public()
try:
    print 'private():', proxy.prefix.private()
except Exception, err:
    print 'ERROR:', err
try:
    print 'public() sin prefijo:', proxy.public()
except Exception, err:
    print 'ERROR:', err

y el resultado, con el mensaje de error que esperábamos atrapado y reportado.

$ python SimpleXMLRPCServer_instance_with_prefix_client.py
public(): Ésto es público
private(): ERROR: <Fault 1: '<type \'exceptions.Exception\'>:metodo "prefix.private" no es soportado'>
public() sin prefijo: ERROR: <Fault 1: '<type \'exceptions.Exception\'>:metodo "public" no es soportado'>

Hay otras maneras de redefinir el mecanismo de despacho, incluyendo el derivar directamente de SimpleXMLRPCServer. Revisa los docstrings en el módulo para más detalles.

Interfaz de introspección

Como con otros servicios de red, es posible consultar un servidor XML-RPC para preguntarle qué métodos soporta y aprender cómo usarlos. SimpleXMLRPCServer incluye un conjunto de métodos públicos para la realización de esta introspección. Por defecto están desactivadas, pero pueden ser activadas con register_introspection_functions(). Puedes añadir soporte explícito para system.listMethods() y system.methodHelp() definiendo _listMethods() y _methodHelp() en tu clase de servicio. Por ejemplo:

from SimpleXMLRPCServer 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) => [<archivos>]

        Retorna una lista con el conteniendo del directorio nombrado.
        """
        return os.listdir(dir_name)

server.register_instance(DirectoryService())

try:
    print 'Usa Control-C para salir'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Saliendo'

En este caso, la función list_public_methods() explora una instancia par devolver los nombres de atributos invocables que no empiezan con "_". Puedes, por supuesto, redefinir _listMethods() para aplicar las reglas que prefieras. Similarmente, para este ejemplo básico _methodHelp() devuelve el docsting de la función, pero podría ser redefinido para construir una cadena de ayuda de cualquier información que tú quieras.

Este cliente consulta al servidor y reporta todos los métodos disponibles públicamente.

import xmlrpclib

proxy = xmlrpclib.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

Nota que los métodos de system están incluidos en los resultados.

$ python SimpleXMLRPCServer_introspection_client.py
============================================================
list
------------------------------------------------------------
list(dir_name) => [<archivos>]

Retorna una lista con el conteniendo del directorio nombrado.

============================================================
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.

Referencias

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