Servicios Web en Python – Parte 4

En el capítulo anterior, tratamos el modo de depuración, los «namespace», objetos y parámetros sin nombre, y los tipos elaborados de SOAPpy.

En esta entrega finalizamos la descripción de SOAPpy, mostrando el manejo del archivo WDSL, y algunas configuraciones útiles del cliente y el servidor.

Archivo WSDL

Un proveedor de Web Service, expone diversas funciones, que deben ser llamadas con su nombre y parámetros correctos (A estas alturas, ya debemos saber que pasa si nos equivocamos en una simple letra de la función remota). Nuestros ejemplos han sido sencillos, uno o cero parámetros de entrada y una cadena de salida. Sin embargo SOAP se diseñó para manejar tipos de datos mas elaborados.

Pero ¿Cómo saber que funciones están disponibles? ¿Qué parámetros espera? ¿En qué “namespace” se encuentra una función? En la práctica, suele pasar que ignoremos por completo que servicios expone un proveedor de Web Service, que no tengamos acceso a la documentación y que no conozcamos al programador. En cualquier caso, debe existir una forma de acceder a esta información.

La respuesta es: Hay que leer el archivo WSDL.

Un archivo WSDL (Web Services Description Language) es un simple texto que permite describir un Web Service de SOAP (aunque se diseñó para soportar diversos protocolos). Físicamente es un archivo en formato XML que suele residir en el servidor que provee el Web Service.

Un archivo WSDL contiene:

  • La URL de las funciones remotas
  • El “namespace” de las funciones remotas.
  • El tipo de Web Service
  • La lista de funciones disponibles.
  • Los argumentos para cada función.
  • El tipo de dato para cada argumento.
  • El valor de retorno (y tipo) de cada función

Obtener un archivo WDSL, no es difícil. Basta con buscar por la Web. Por ejemplo, una dirección puede ser (Es probables que esta dirección haya cambiado. Si no se encuentra disponible, se debe actualizar con la nueva URL.):

http://staff.um.edu.mt/cabe2/supervising/undergraduate/owlseditFYP/TemperatureService.wsdl

Al escribir esta URL por el navegador, obtendremos el texto del archivo WDSL:

<?xml version="1.0"?>
<definitions name="TemperatureService" targetNamespace="http://www.xmethods.net/sd/TemperatureService.wsdl"  xmlns:tns="http://www.xmethods.net/sd/TemperatureService.wsdl"   xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/">
	<message name="getTempRequest">
		<part name="zipcode" type="xsd:string"/>
	</message>
	<message name="getTempResponse">
		<part name="return" type="xsd:float"/>
	</message>
	<portType name="TemperaturePortType">
		<operation name="getTemp">
			<input message="tns:getTempRequest"/>
			<output message="tns:getTempResponse"/>
...

Si bien se puede leer toda la información del texto XML. Es preferible interpretar (parse) el XML, usando las funcionalidades de SOAPpy.

Quizá la información más importante que podamos conseguir de un WSDL, es la lista de funciones disponibles. Hagamos una verificación de las funciones disponibles en el Web Service del ejemplo anterior:

#################  CLIENTE ##################

from SOAPpy import WSDL
wsdlFile = 'http://staff.um.edu.mt/cabe2/supervising/undergraduate/owlseditFYP/TemperatureService.wsdl'
server = WSDL.Proxy(wsdlFile)
print server.methods.keys()

Observar que ahora usamos el objeto WSDL para crear un Proxy. Cuando ejecutamos WSDL.Proxy(wsdFIle), el cliente descargará el archivo WSDL y lo interpretará para obtener información sobre los Servicios Web.

Este código mostrará en pantalla:

[u'getTemp']

Que significa que la única función, que está disponible es getTemp(), lo cual es lógico porque esta dirección Web sirve para dar la temperatura.  Si hubieran más funciones disponibles nos aparecería una lista como:     

[u' getTemp ', u' getTemp2']

Ahora que sabemos que existe una función llamada getTemp(), podemos hacer la llamada correcta. Pero antes debemos saber los parámetros. Para indagar sobre los parámetros de la función, podemos escribir:

print proxy.methods["getTemp"].inparams

Que nos da el siguiente mensaje:

[<wstools.WSDLTools.ParameterInfo instance at 0x0249C3A0>]

Está claro, que no se puede obtener mucha información de esta forma. Para obtener el nombre del parámetro y el tipo, debemos usar:

print server.methods["getTemp"].inparams[0].name
print server.methods["getTemp"].inparams[0].type

Ahora obtenemos un texto más descriptivo:

zipcode
(u'http://www.w3.org/2001/XMLSchema', u'string')

La primera línea corresponde al nombre del parámetro. Por su nombre deducimos que se espera el código postal de alguna ciudad.

La segunda línea indica el tipo del parámetro.  En este caso debemos interpretarla como “string”. De la misma forma para obtener el nombre y tipo del valor devuelto, debemos hacer:

print server.methods["getTemp"].outparams[0].name
print server.methods["getTemp"].outparams[0].type

La respuesta en este caso sería:

return
(u'http://www.w3.org/2001/XMLSchema', u'float')

Que nos indica que el valor devuelto es de tipo “float”.

Ahora con esta información, ya es posible acceder correctamente a la función getTemp(), y procesar su resultado. No hay más magia alrededor de WSDL.

El siguiente código, incluye todas lecturas anteriores al WSDL:

from SOAPpy import WSDL
wsdlFile= 'http://staff.um.edu.mt/cabe2/supervising/undergraduate/owlseditFYP/TemperatureService.wsdl'
server = WSDL.Proxy(wsdlFile)
print server.methods.keys()
print server.methods["getTemp"].inparams
print server.methods["getTemp"].inparams[0]
print server.methods["getTemp"].inparams[0].name
print server.methods["getTemp"].inparams[0].type
print server.methods["getTemp"].outparams[0].name
print server.methods["getTemp"].outparams[0].type

También podemos llamar a una función remota a través del objeto WSDL:

#################  CLIENTE ##################

from SOAPpy import WSDL
wsdlFile = 'http://staff.um.edu.mt/cabe2/supervising/undergraduate/owlseditFYP/TemperatureService.wsdl'
server = WSDL.Proxy(wsdlFile)
print server.getTemp('90210')

Desde luego que, si ya conocemos el nombre y parámetros de la función remota; no valdría la pena usar este tipo de llamada, ya que además consumiría recursos adicionales al descargar e interpretar el WSDL.

La única ventaja, que tal vez  podríamos encontrar en este tipo de llamada de un Web Service, es que no es necesario ya especificar el “namespace”, porque este, ya es leído del WSDL. También la URL de la función se toma del contenido del WSDL.

Otro ejemplo de llamada  a través de WDSL sería:

#################  CLIENTE ##################
from SOAPpy import WSDL
wsdlFile= 'http://www.w3schools.com/webservices/tempconvert.asmx?WSDL'
server = WSDL.Proxy(wsdlFile)
print server.FahrenheitToCelsius(100)

Hay que agregar que podría darse el caso de que la descripción en el WDSL no corresponda con las funciones que realmente existen. El archivo WDSL es un contrato, pero no es infalible.

Además podría haber incompatibilidad en los formatos de los XML request y response, por el lado del cliente y servidor. Cada servidor tiene su forma de responder a los errores. Algunos amablemente mandarán un mensaje como resultado de la función. Otros generarán excepciones y otros simplemente no responderán.

Es conveniente siempre asegurarse antes, de que el archivo WDSL existe, usando un navegador común. Si se puede visualizar en el navegador, significa que el WDSL está disponible.

Creando un WDSL para mi servidor

Hasta ahora hemos usado un WDSL existente en algún servidor remoto, desde el lado del cliente. Pero ¿Y qué pasa con el Servidor?

Si queremos exponer nuestros Servicios Web, a través de un WDSL, lo más lógico es que contemos con uno. Y también que podamos publicarlo para que sea libremente accesible.

Desgraciadamente la librería SOAPpy, no provee de un medio sencillo para generar un archivo WSDL, ni para publicarlo en Web.

Pero no nos desanimemos. Existen diversas formas de solucionar estos problemas:

Primer Problema: Obtener un archivo WSDL

La idea es tener un WSDL que describa las funciones que tenemos expuestas. Las posibles soluciones son:

  1. Descargar un editor para WSDL, como el “Eclipse“, con sus componentes para Web.
  2. Descargar un WDSL de funciones similares a las nuestras y modificarlo, “a mano”.
  3. Coger un editor de texto y crear uno desde cero. No es la opción recomendable, pues un archivo WSDL no es tan simple como un html.

Por si se anima, aquí muestro un modelo de archivo WSDL de tipo «Hola Mundo» seria:

<?xml version="1.0"?>
<definitions name="ConsultaService" 
             targetNamespace="http://ivr/"  
			 xmlns:tns="http://ivr/"   
			 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
			 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
			 xmlns="http://schemas.xmlsoap.org/wsdl/">
	<message name="ConsultaRequest">
		<part name="parametro1" type="xsd:string"/>
	</message>
	<message name="ConsultaResponse">
		<part name="Result" type="xsd:string"/>
	</message>
	<portType name="ConsultaPortType">
		<operation name="HolaMundo">
			<input message="tns:ConsultaRequest"/>
			<output message="tns:ConsultaResponse"/>
		</operation>
	</portType>
	<binding name="ConsultaBinding" type="tns:ConsultaPortType">
		<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
		<operation name=" HolaMundo ">
			<soap:operation soapAction=""/>
			<input>
				<soap:body use="encoded" namespace="http://ivr/" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
			</input>
			<output>
				<soap:body use="encoded" namespace="http://ivr/" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
			</output>
		</operation>
	</binding>
	<service name="ConsultaService">
		<documentation>Este es un ejemplo sencillo de archivo WSDL de una sola función de un solo parametro</documentation>
		<port name="ConsultaPort" binding="tns:ConsultaBinding">
			<soap:address location="http://192.168.1.55:8080"/>
		</port>
	</service>
</definitions>

Las partes más importantes se han coloreado. Las partes marcadas en amarillo corresponden al parámetro de entrada (parametro1) y al resultado devuelto. Las partes marcadas en verde corresponden al nombre de la función.  La parte marcada en Azul corresponde al “namespace”. La parte marcada en naranja, es la URL real de la función expuesta.

Para agregar más parámetros de entrada, basta con agregar más líneas de tipo: <part name=»parametro1″ type=»xsd:string»/>

El archivo WSDL mostrado,  sería la descripción de una función de este tipo:

def HolaMundo(parametro1):
    print "petic. atendida"
    return "Hola Mundo"

Está claro que no es una tarea fácil crear estructuras más complejas modificando a mano un WDSL.

Segundo Problema: Publicar el archivo WSDL

Una vez que tenemos nuestro WSDL, lo que queda ahora es poder publicarlo, para que sea accesible a los clientes. SOAPpy, no funciona como servidor de archivos, solo maneja Web Service.

Para publicar nuestro WSDL, podemos:

  1. Usar algún dominio disponible que tengamos en donde podamos depositar archivos, puede ser nuestro o alquilado. (El archivo WSDL no tiene por qué estar en el mismo dominio del Web Service).
  2. Contratar un dominio (o usar uno gratuito) para publicar nuestro WSDL.
  3. Instalar un servidor Web (si es que no se tiene ya uno instalado) en nuestro servidor, que permita el acceso a nuestro archivo WSDL.
  4. Instalar un simple servidor de archivos en nuestro servidor para que pueda dar acceso externo a nuestro WSDL.

Si se trabaja sobre Linux, para el servidor, es fácil publicar cualquier archivo por Web. Solo basta con hacer uso del módulo “SimpleHTTPServer” de Python. Un simple comando bastará para tener un servidor de archivos.

python -m SimpleHTTPServer 8000

Con este comando hacemos pública, a la Web el directorio actual. El acceso se hará a través del puerto 8000.

Hay que considerar que si tenemos el Web Service y un Servidor Web o de archivos, en el mismo servidor, es casi seguro que no podrán compartir el mismo puerto. Ya que una aplicación toma un puerto de forma exclusiva y no estará disponible para otra aplicación, a menos que usemos las configuraciones avanzadas de los servidores Web que permitan  redireccionar algunas peticiones a otro aplicativo en un puerto diferente (como en Apache).

Configurando el Cliente

En el lado del cliente, cada vez que hacemos:

client =SOAPpy.SOAPProxy(service_url, namespace)

Estamos creando una instancia, de SOAPProxy. Este objeto creado tiene diversas propiedades, que se inician con valores por defecto:

  • proxy, string with an URI to the service endpoint (mandatory);
  • namespace=None, string with service namespace;
  • soapaction=None, not really used;
  • header=None, header to be included in the SOAP request;
  • methodattrs=None, attributes describing methods in SOAP requests;
  • transport=<class SOAPpy.Client.HTTPTransport>, the only implemented transport mechanism;
  • encoding=’UTF-8′, encoding;
  • throw_faults=1, config setting causing fault responses to be thrown as exceptions.
  • unwrap_results=None, config setting to causing SOAPProxy to strip the response structure if it cointains just one element (i.e. the element becomes the response structure) ENABLED BY DEFAULT
  • http_proxy=None, specify an http proxy;
  • config=<SOAPpy.Config.SOAPConfig instance>, see Client Config;
  • noroot=0, if this is set to 1 root attributes are not added to typed objects;
  • simplify_objects=None, Automatically simplfy SOAP complex types into the corresponding python types. (structType -> dict, arrayType –> array, etc.) See SOAPpy types for more info on types.

En ciertos casos, sin embargo, habrá que cambiar algún de estos valores. No existe mucha documentación sobre estas propiedades (al menos al tiempo de escribir este artículo, allá por el año 2013), así que en algunos de casos habrá que hacer ensayo y error. Existen diversas opciones de configuración que se pueden aplicar al campo “config”:

Attribute (default)TypeMeaning
authMethod (None)codeCall the specified authorization method preceding each service call.
buildWithNamespacePrefixBooleanInstruct SOAPBuilder to add namespace to typed tags
debug (0)BooleanSet dump* attributes to its own’s value
dict_encodingStringSet default encoding for dictionary keys, default
dumpFaultInfoBooleanDump fault information.
dumpHeadersInBooleanDump incoming headers
dumpHeadersOutBooleanDump outgoing headers
dumpSOAPInBooleanDump incoming XML
dumpSOAPOutBooleanDump outgoing XML
namespaceStyle‘1999’ ‘2000’ ‘2001’Setting this parameter sets typesNamespace, typesNamespaceURI, schemaNamespace, and schemaNamespaceURI accordingly
returnAllAttrs
returnFaultInfo
schemaNamespacestring
schemaNamespaceURIstring
simplify_objectsBooleansimplfy SOAP complex types into the corresponding python types
specialArgs
strictNamespaces
strict_rangeBooleanStrictly check the range for floats and doubles
typedBooleanEmbed XSI types in returned objects
typesNamespace
typesNamespaceURI
unwrap_resultsBooleanMake SOAPProxy strip the response structure if it cointains one single element (i.e. the element becomes the response structure)

La propiedad “typed” del objeto “config”, permite definir si el tipo de parámetro enviado, se incluye o no en el XML.

Por ejemplo, el siguiente código, generará una llamada a la función Saludo(), sin incluir el tipo del parámetro:

import SOAPpy
server = SOAPpy.SOAPProxy("http://localhost:8080/")
SOAPpy.Config.typed = False   #no incluye información de tipo en XML
print server.Saludo(‘mi nombre’)

El lado remoto, sin embargo, deberá tener definida la función Saludo(), con el parámetro de tipo texto, para que no se generen errores.

Cliente y Servidor en Servidores Distintos

En los ejemplos anteriores, hemos visto como se conectan el cliente y servidor de Web Service, pero instalados ambos en una misma estación de trabajo. Por eso las direcciones eran “localhost”.

En la práctica, tendremos que el cliente y servidor son siempre máquinas distintas y alejadas. Para este caso, debemos configurar los accesos, del cliente y servidor en modo distinto:

#################  SERVIDOR ##################
import SOAPpy

#función que retorna el doble
def doble(x):
    print "petic. atendida"
    return 2*x

server = SOAPpy.SOAPServer(("192.168.1.50", 8080))
server.registerFunction(doble)
print "Esperando peticiones"
server.serve_forever()

#################  CLIENTE ##################
import SOAPpy
server = SOAPpy.SOAPProxy("http://192.1638.1.50:8080")
print server.doble(5)

Para el servidor, es preferible usar la dirección física real del servidor. Usar “localhost”, podría no funcionar. El puerto a usar, debe estar abierto y disponible.

En el cliente, se especifica también la dirección IP del servidor remoto. Lógicamente el número de puerto debe coincidir.

Si el servidor remoto, tiene un dominio o URL asociado, se puede indicar ese dominio:

from SOAPpy import SOAPProxy  
url = 'http://services.xmethods.net:80/soap/servlet/rpcrouter'
namespace = 'urn:xmethods-Temperature'
server = SOAPProxy(url, namespace)

Cuando se omite el puerto, se supone que se usará el puerto 80.

Concurrencia de peticiones

Un proveedor de Web Service, no puede atender las peticiones a velocidad infinita. Siempre le tomará un tiempo responder a las solicitudes. Lo común es que la respuesta sea lo más rápida posible, pero en la práctica siempre existen retrasos, y algunos de ellos no necesariamente dependen del servidor (puede ser problema de lentitud en la red).

Cuando usamos SOAPpy, en la forma en la que hemos estado trabajando, no se pueden manejar peticiones en paralelo. Siempre una petición debe ser atendida, antes de pasar a atender  a la siguiente petición.

Cuando se genera una petición, antes de que termine la anterior, se genera una especie de cola de espera, de modo que, apenas se termine de ejecutar la petición actual, se atenderá a la siguiente en la cola.

Referencias

Información sobre SOAPy: https://github.com/kiorky/SOAPpy/tree/develop/docs


Sé el primero en comentar

Dejar una contestacion

Tu dirección de correo electrónico no será publicada.


*