Python in architetture SOA
Note: Return to tutorial view.
Web Services
Il web service è, sopratutto in ambito enterprise, il sistema preferito per fare interoperare più applicazioni tra di loro. Utilizzando questo sistema le applicazioni possono essere su diversi pc e possono essere realizzate con diverse tecnologie.
La libreria standard di python supporta generosamente questo tipo di tecnologia attraverso un package dedicato al protocollo XMLRPC e molti framework e application server supportano nativamente questo protocollo (ad esempio Zope).
Talvolta, specialmente per colloquiare con il mondo Java o .Net, è preferibile utilizzare il protocollo SOAP (certamente meglio supportato/conosciuto in queste realtà).
Vedremo nelle prossime pagine come utilizzare gli strumenti che ci consentono di interfacciare la nostra applicazione attraverso il protocollo SOAP.
Come prima cosa dovremo però descrivere WSGI: il sistema che consente di esporre una applicazione Python attraverso un web server. In seguito vedremo come realizzare una semplice interfaccia web con WSGI e infine come sfruttare WSGI per il nostro web service.
Ma, andiamo per ordine, cos'è un web service?
Web service
Come spiegato su wikipedia un web service è un interfaccia software che permette di richiamare una funzione o di utilizzare un oggetto che può risiedere su un altro pc e può essere scritto con un altro linguaggio.
Il sistema si basa sullo scambio via rete di messaggi tra le due applicazioni: l'applicazione chiamante è il client e l'applicazione che offre il servizio è il server.
Il protocollo utilizzato è http che è lo stesso utilizzato per le pagine web (da qui il termine web service). Tramite http viene fatta una richiesta al web server (dove viene "esposto" il servizio) con gli argomenti passati in un file xml. Dopo l'elaborazione, il web server risponderà con un altro xml contenente il risultato.
Il protocollo soap prevede anche l'esposizione di un file WSDL (Web Services Description Language) che serve a descrivere l'interfaccia pubblica dell'oggetto e permettendone, lato client, l'introspezione.
Le librerie per la gestione dei web services (come ad esempio SOAPLIB) svolgono queste attività dietro le quinte, in modo trasparente.
WSGI e applicazioni web in Python
WSGI
WSGI sta per python Web Server Gateway Interface. E' il modo standard in cui un applicazione Python si interfaccia con un Web server.
WSGI è supportato, oltre che dal modulo wsgiref della libreria standard, da Apache tramite il modulo mod-wsgi , da nginx (in modo sperimentale), da cherrypy da twisted e da altri.
Un applicazione wsgi è fatta così:
def my_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type','text/plain')]
start_response(status, response_headers)
return ["hello world"]
si tratta semplicemente di una funzione che prende in input un dizionario environ contenente i parametri di ambiente.
Di seguito riporto, come esempio, il contenuto di environ riportato su una mia applicazione (la pagina richiamata è http://localhost:7789/):
wsgi.multiprocess=False wsgi.multithread=True SERVER_SOFTWARE=CherryPy/3.1.1 WSGI Server SCRIPT_NAME=/admin/make_pdf ACTUAL_SERVER_PROTOCOL=HTTP/1.1 wsgi.input=<cherrypy.wsgiserver.SizeCheckWrapper object at 0x013180D0> REQUEST_METHOD=GET HTTP_HOST=localhost:7789 PATH_INFO= SERVER_PROTOCOL=HTTP/1.1 QUERY_STRING= wsgi.version=(1, 0) HTTP_ACCEPT=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 HTTP_ACCEPT_CHARSET=ISO-8859-1,utf-8;q=0.7,*;q=0.7 HTTP_USER_AGENT=Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8 HTTP_CONNECTION=keep-alive SERVER_NAME=D01C100164 REMOTE_ADDR=127.0.0.1 wsgi.run_once=False wsgi.errors=<open file '<stderr>', mode 'w' at 0x00A100B0> REMOTE_PORT=1780 HTTP_ACCEPT_LANGUAGE=it-it,it;q=0.8,en-us;q=0.5,en;q=0.3 wsgi.url_scheme=http SERVER_PORT=7789 HTTP_ACCEPT_ENCODING=gzip,deflate HTTP_KEEP_ALIVE=300
Come argomento viene passato anche un reference ad una funzione (start_response) che dovrà essere utilizzata per inizializzare la risposta: start_response prende in input una stringa che dovrà contenere l'http status e una lista di tuple (chiave valore) che fanno parte dell' intestazione http. Come output della funzione verrà restituito un iterabile (ad esempio una lista). Questa caratteristica rende possibile utilizzare yield al posto di return (questo può favorire l'utilizzo di webserver asincroni come nginx o twisted . . . ma questa è un altra storia).
Ecco un applicazione wsgi molto semplice di esempio che restituisce una pagina web.
Si tratta di un form che richiede un nome, premendo submit verrà proposta una pagina con Hello seguito dal nome scelto.
Vediamo il codice:
Come prima cosa definiamo un template che farà da base a tutte le nostre pagine html.
html_template="""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="it" lang="it"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta name="author" lang="it" content="Maurizio Lupo" /> <title>My web interface</title> <link rel="stylesheet" media="screen,projection" type="text/css" href="/static/default.css" /> </head> <body> <div id="header"><img src="/logo.png" /></div> <div id="menu"> <a href="/admin">Main menu</a> <a href="/admin/about">About</a> </div> <div id="main"> %s </div> <div id="footer"> My web interface </div> </body> </html>"""
Riporto una semplice funzione che trasforma la query string in un dizionario:
ad esempio: http://miosito/applicazione?nome=mickey+mouse&indirizzo=house+of+mouse diventerà
{nome=mickey mouse,indirizzo=house of mouse}
from urllib import unquote_plus
def get_args(data):
ret = {}
for val in data.split('&'):
val = val.split('=', 1)
k = val[0].lower()
if len(val) > 1: v = val[1]
else: v = ''
ret[k] = unquote_plus(v)
return ret
webinterface è la mia semplicissima applicazione WSGI realizzata come autosubmitting for: in base alla presenza dei dati richiesti nella query string sottoporrò la form con la richiesta dei dati oppure risponderò con l'elaborazione dei dati.
def webinterface(environ, start_response):
args=get_args(environ['QUERY_STRING'])
if 'name' in args:
content="<h1>Hello %s</h1>" % (args['name'])
else:
content="""
<div class="form">
<div class="title">Form</div>
<form action="/admin/new" method="get">
<div class="label">Name:</div><input type="text" name="Name" /><br />
<input type="submit" value="Submit" />
</form>
</div>
"""
status = '200 OK'
response_headers = [('Content-type','text/html')]
start_response(status, response_headers)
return [html_template % (content,)]
Nella prossima pagina vedremo come pubblicare un applicazione scritta in questo modo con il web server WSGI CheeryPy.
CherryPy WSGI server
Proseguendo con l`esempio della pagina precedente ho scelto di interfacciarmi con il web server incluso nel framework CherryPy.
Questo web server ha delle performance molto interessanti ed è molto semplice da integrare. Ecco come (mi riferisco alla versione 3):
from cherrypy.wsgiserver import CherryPyWSGIServer
server = CherryPyWSGIServer(("0.0.0.0",8080),webinterface)
server.start()
Nel precedente esempio, dopo aver importato la classe CherryPyWSGIServer dal package cherrypy.wsgiserver, ho creato un oggetto server dando in input una tupla con url e porta http (in questo caso 8080) e il reference alla mia applicazione wsgi.
Poi ho avviato il server con il metodo start. Il processo si fermerà sul metodo start finchè non verrà terminato il server.
Per fermare il server basterà chiamare (da un altro thread) il metodo stop. Questo terminerà tutti i thread del server in modo "aggrazziato". Per fare un test è possibile terminare il server in risposta alla pressione di ctrl-c in questo modo:
try: server.start() except KeyboardInterrupt: server.stop(
In questo modo tutte le chiamate al nostro web server vengono gestite dall'applicazione webinterface.
E' possibile mappare le applicazioni wsgi da utilizzare in base agli url utilizzando l'oggetto WSGIPathInfoDispatcher:
from cherrypy.wsgiserver import CherryPyWSGIServer, WSGIPathInfoDispatcher
apps = WSGIPathInfoDispatcher({'/': webinterface,
'/app2': app2,
'/app3':app3})
server = CherryPyWSGIServer(("0.0.0.0",8080),apps)
server.start()
WSGIPathInfoDispatcher prende in input un semplice dizionario e in base alla corrispondenza della chiave con l'URL richiama diverse applicazioni.
In questo caso all`url / verrà richiamata la funzione webinterface mentre agli url /app2 e /app3 verranno richiamate rispettivamente le funzioni app2 e app3.
Risorse statiche
In tutto questo processo manca il modo di reperire delle risorse statiche (pagine html, css, immagini ecc.). Per ottenere questo ho utilizzato un'applicazione wsgi specializzata in questo compito. Si trova sul cheese shop e si ma è possibile scaricarla da qui.
Ho quindi modificato la mia applicazione così:
from static import Cling
path="/mystaticpath"
static_res = Cling(path)
apps = WSGIPathInfoDispatcher({'/': webinterface,
'/app2': app2,
'/app3':app3,
'/static':static_res})
server = CherryPyWSGIServer(("0.0.0.0",8080),apps)
server.start()In questo modo le risorse statiche che si trovano in locale nella directory /mystaticpath verranno rese disponibili all`url /static. Ad esempio, volendo usare un foglio di stile dovrei metterlo sotto /mystaticpath/default.css e farlo richiamare all`url /static/default.css.
Usare SoapLib
Soaplib è una libreria molto semplice e "pythonica" per scrivere web service soap come applicazioni wsgi.
Ecco un primo esempio riportato dalla documentazione ufficiale:
from soaplib.wsgi_soap import SimpleWSGISoapApp
from soaplib.service import soapmethod
from soaplib.serializers.primitive import String, Integer, Array
class HelloWorldService(SimpleWSGISoapApp):
@soapmethod(String,Integer,_returns=Array(String))
def say_hello(self,name,times):
results = []
for i in range(0,times):
results.append('Hello, %s'%name)
return results
if __name__=='__main__':
from cherrypy.wsgiserver import CherryPyWSGIServer
server = CherryPyWSGIServer(('localhost',7789),HelloWorldService())
server.start()
Per utilizzare l'oggetto da remoto dovrò fare:
>>> from soaplib.client import make_service_client
>>> from helloworld import HelloWorldService
>>> client = make_service_client('http://localhost:7789/',HelloWorldService())
>>> print client.say_hello("Dave",5)
['Hello, Dave','Hello, Dave','Hello, Dave','Hello, Dave','Hello, Dave']
Analizziamo ora l'esempio:
Il metodo say_hello della classe HelloWorldService sarà esposto pubblicamente e, una volta ottenuto un istanza dell'oggetto da remoto con client = make_service_client('http://localhost:7789/',HelloWorldService())
potrò richiamare il metodo con client.say_hello(...) utilizzando la classe HelloWorldService come se fosse definita in locale.
Da quello che abbiamo visto precedentemente questo oggetto non assomiglia ad una applicazione wsgi !!!
La magia sta dietro alla superclasse SimpleWSGISoapApp. Questa ha infatti un metodo speciale __call__ che è la nostra funzione wsgi:
def __call__(environ, start_response) ...
Infatti bisogna notare che viene passata un istanza di HelloWorldService
server = CherryPyWSGIServer(('localhost',7789),HelloWorldService())
e non un riferimento
server = CherryPyWSGIServer(('localhost',7789),HelloWorldService) # errato !!!!
I decoratori @soapmethod servono a definire quali metodi rendere pubblici e con quali tipi standard devono essere mappati.
I tipi utilizzabili sono contenuti nel package serializers. Sono presenti tipi primitivi come Integer, String, Float, DateTime, Boolean.
E' possibile definire utilizzare delle liste tramite il tipo Array. In questo caso è necessario che la lista contenga valori dello stesso tipo. Per esempio Array(String) sarà un array di stringhe.
L'argomento _return determinerà il tipo del valore di ritorno del metodo.
Tornando al nostro esempio
@soapmethod(String,Integer,_returns=Array(String))
def say_hello(self,name,times):
. . .
Il metodo say_hello prenderà come argomenti:
- uno String mappato come stringa nel parametro name
- un Integer mappato come intero nel parametro times
e restituirà in output Array(String) che verrà mappato come una lista di stringhe
ecco infatti il client
>>> print client.say_hello("Dave",5)
['Hello, Dave','Hello, Dave','Hello, Dave','Hello, Dave','Hello, Dave'
Utilizzare tipi complessi, WSDL e conclusioni
Passaggio di binari
Il protocollo http non prevede lo scambio di valori binari. Qualsiasi cosa venga trasmessa deve quindi essere trasformata con una codifica chiamata Base64 che trasforma un flusso binario in un flusso ascii. Fortunatamente il passaggio di dati binari viene gestito automaticamente attraverso il tipo Attachment.
L'oggetto attachment è dotato di una serie di metodi appositi per la gestione di file.
Vediamo due esempi dalladocumentazione
class DocumentArchiver(SimpleWSGISoapApp):
@soapmethod(Attachment,_returns=String)
def archive_document(self,document):
'''
This method accepts an Attachment object, and returns the filename of the
archived file
'''
fd,fname = mkstemp()
os.close(fd)
document.fileName = fname
document.save_to_file()
return fname
@soapmethod(String,_returns=Attachment)
def get_archived_document(self,file_path):
'''
This method loads a document from the specified file path
and returns it. If the path isn't found, an exception is
raised.
'''
if not os.path.exists(file_path):
raise Exception("File [%s] not found"%file_path)
document = Attachment(fileName=file_path)
# the service automatically loads the data from the file.
# alternatively, The data could be manually loaded into memory
# and loaded into the Attachment like:
# document = Attachment(data=data_from_file)
return document
Questo oggetto contiene due metodi:
Il primo "archive_document" prende in input un file, lo salva in una cartella temporanea e restituisce il nome del file.
Il secondo "get_archived_document" prende in input il nome di un file e restituisce il file.
Il client può sfruttare il server in questo modo:
# client è la nostra istanza remotefilename=client.archive_document(Attachment(fileName="mydata")
mydata è il nome del mio file locale e remotefilename è il nome del file sul sistema remoto.
Posso anche passare al costruttore direttamente una stringa di dati binari.
remotefilename=client.archive_document(Attachment(data=open("binaryfile").read()
Posso recuperare il mio file con
myfile=client.get_archived_document(remotefilename)
# myfile è un oggetto di tipo Attachment
myfile.save_to_file("mydata"
Utilizzare dei tipi composti
E' possibile definire dei tipi composti. Per farlo è necessario che la nostra classe estenda ClassSerializer (from soaplib.serializers.clazz) e definisca una sottoclasse di nome types con un elenco di attributi (semplici o, a loro volta, composti).
Ad esempio:
class Permission(ClassSerializer): class types: application = String feature = String class User(ClassSerializer): class types: userid = Integer username = String firstname = String lastname = String permissions = Array(Permission)
Questi nuovi tipi potranno essere usati nei nostri decoratori soapmethod:
@soapmethod(User)
o
@soapmethod(_returns=Array(User))
Queste classi speciali funzionano come classi normali:
u=User() u.username="Pippo" u.userid=3
Gli attributi non inizializzati saranno però valorizzati None.
Generare WSDL
I wsdl sono dei file xml che contengono la descrizione dell'oggetto: i metodi esposti, la quantità e il tipo degli argomenti e dei valori di ritorno. Tramite questi file su Eclipse, utilizzando Java ad esempio, è possibile farsi costruire automaticamente la classe client.
Soaplib dispone di due modi di generare il wsdl: il primo direttamente dal server aggiungendo "wsdl" all'indirizzo della nostra risorsa. Tornando al nostro primo esempio:
>>> client = make_service_client('http://localhost:7789/',HelloWorldService())
>>> print client.say_hello("Dave",5)
Visitando l'indirizzo http://localhost:7789/wsdl ci verrà restituito il file wsdl.
Il secondo modo per ottenere il wsdl e tramite la funzione
client.server.wsdl('')
In questo modo il wsdl viene generato lato client.
Conclusioni
Direi che abbiamo affrontato tutti i punti relativi all'interfacciamento di un applicazione python tramite web service.
- come usare un web server wsgi (in questo caso cherrypy)
- come scrivere un`interfaccia web (a basso livello con WSGI)
- come esporre degli oggetti tramite il protocollo soap (usando soaplib)
Approfondimenti
Per approfondire l'argomento dei web service può essere interessante esplorare altri tipi di web service, ad esempio xmlrpc (librerie xmlrpc e pyro) o altre implementazioni di soap per python (suds, zsi, twisted ).
