PyMOTW: SimpleXMLRPCServer
9 Julio 2008
Traducción de PyMOTW: SimpleXMLRPCServer el módulo SimpleXMLRPCServer de la columna semanal de Doug Hellmann.
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
