Chapter 10: Services

Services

Web Services
API

Le W3C définit un web service comme "un logiciel système destiné à supporter l'inter-opérabilité des interactions machine à machine sur un réseau". C'est une définition large, et embarque un large nombre de protocoles qui ne sont pas destinés à la communication de machine à humain, mais bien pour la communication entre machines tels que XML, JSON, RSS, etc...

Dans ce chapitre, nous présentons comment exposer les web services en utilisant web2py. Si vous êtes intéressé par des exemples d'utilisation de services tiers (Twitter, Dropbox, etc...) vous devriez regarder le Chapitre 9 et le Chapitre 14.

web2py fournit, de base, le support pour de nombreux protocoles, incluant XML, JSON, RSS, CSV, XMLRPC, JSONRPC, AMFRPC, et SOAP. web2py peut également être complété pour ajouter le support de protocoles additionnels.

Chacun de ces protocoles sont supportés de différentes manières, et nous ferons une distinction entre :

  • Le rendu de sortie de fonction dans un format donné (for example XML, JSON, RSS, CSV)
  • Les Remote Procedure Calls (par exemple XMLRPC, JSONRPC, AMFRPC)

Retourner un dictionnaire

HTML, XML, et JSON

HTML
XML
JSON

Considérons l'action suivante :

def count():
    session.counter = (session.counter or 0) + 1
    return dict(counter=session.counter, now=request.now)

Cette action retourne un compteur qui est incrémenté d'un lorsqu'un visiteur recharge la page, et le timestamp de la requête de la page courante est mis à jour.

Normalement cette page devrait être appelée via :

http://127.0.0.1:8000/app/default/count

et rendue en HTML. Sans écrire une seule ligne de code, nous pouvons demander à web2py de rendre cette page en utilisant différents protocoles en ajoutant une extension à l'URL :

http://127.0.0.1:8000/app/default/count.html
http://127.0.0.1:8000/app/default/count.xml
http://127.0.0.1:8000/app/default/count.json

Le dictionnaire retourné par l'action sera rendu en HTML, XML, et JSON respectivement.

Voici la sortie XML :

<document>
   <counter>3</counter>
   <now>2009-08-01 13:00:00</now>
</document>

Voici la sortie JSON :

{ 'counter':3, 'now':'2009-08-01 13:00:00' }

Notez que les objets date, time et datetime sont rendus en chaînes au format ISO. Ceci ne fait pas partie du standard JSON, mais plutôt d'une convention web2py.

Vues génériques

Lorsque, par exemple, l'extension ".xml" est appelée, web2py cherche un fichier template appelé "default/count.xml", et s'il ne le trouve pas, il cherche un template appelé "generic.xml". Les fichiers "generic.html", "generic.xml", "generic.json" sont fournis avec l'application de référence courante. D'autres extensions peuvent facilement être définies par l'utilisateur.

Pour des raisons de sécurité, les vues génériques ne sont autorisées qu'en localhost. Afin d'activer leur accès depuis des clients distants, vous pouvez avoir besoin de définir le response.generic_patterns.

Supposant que vous utilisez une copie de l'application de référence, éditez la ligne suivante dans models/db.py

  • restreindre l'accès uniquement à localhost
response.generic_patterns = ['*'] if request.is_local else []
  • pour autoriser toutes les vues génériques
response.generic_patterns = ['*']
  • pour n'autoriser que .json
response.generic_patterns = ['*.json']

Le generic_patterns est un pattern global, cela signifie que vous pouvez utiliser n'importe quel pattern qui correspond aux actions de votre application ou passer une liste de patterns.

response.generic_patterns = ['*.json','*.xml']

Pour l'utiliser dans une application web2py plus ancienne, vous pouvez avoir besoin de copier les fichiers "generic.*" depuis une application de référence plus récente (après v1.60).

Voici le code pour "generic.html"

{{extend 'layout.html'}}

{{=BEAUTIFY(response._vars)}}

<button onclick="document.location='{{=URL("admin","default","design",
args=request.application)}}'">admin</button>
<button onclick="jQuery('#request').slideToggle()">request</button>
<div class="hidden" id="request"><h2>request</h2>{{=BEAUTIFY(request)}}</div>
<button onclick="jQuery('#session').slideToggle()">session</button>
<div class="hidden" id="session"><h2>session</h2>{{=BEAUTIFY(session)}}</div>
<button onclick="jQuery('#response').slideToggle()">response</button>
<div class="hidden" id="response"><h2>response</h2>{{=BEAUTIFY(response)}}</div>
<script>jQuery('.hidden').hide();</script>

Voici le code pour "generic.xml"

{{
try:
   from gluon.serializers import xml
   response.write(xml(response._vars),escape=False)
   response.headers['Content-Type']='text/xml'
except:
   raise HTTP(405,'no xml')
}}

Et voici le code pour "generic.json"

{{
try:
   from gluon.serializers import json
   response.write(json(response._vars),escape=False)
   response.headers['Content-Type']='text/json'
except:
   raise HTTP(405,'no json')
}}

N'importe quel dictionnaire peut être rendu en HTML, XML et JSON tant qu'il ne contient que les types primitifs Python (int, float, string, list, tuple, dictionary). response._vars contient le dictionaire retourné par l'action.

Si le dictionnaire contient d'autres objets définis par l'utilisateur ou spécifiques à web2py, ils doivent être rendus par une vue personnalisée.

Rendre les Rows

as_list

Si vous avez besoin de rendre un ensemble de Rows comme retourné par un select au format XML, JSON, ou tout autre format, transformez d'abord l'objet Rows en une liste de dictionnaires en utilisant la méthode as_list().

Considérez par exemple le mode suivant :

db.define_table('person', Field('name'))

L'action suivante peut être rendue en HTML, mais pas en XML ou JSON :

def everybody():
    people = db().select(db.person.ALL)
    return dict(people=people)

alors que l'action suivante peut être rendue en XML et JSON :

def everybody():
    people = db().select(db.person.ALL).as_list()
    return dict(people=people)

Formats personnalisés

Si, par exemple, vous voulez rendre une action comme pickle Python :

http://127.0.0.1:8000/app/default/count.pickle

Vous avez juste besoin de créer un nouveau fichier de vue "default/count.pickle" qui contient :

{{
import cPickle
response.headers['Content-Type'] = 'application/python.pickle'
response.write(cPickle.dumps(response._vars),escape=False)
}}

Si vous voulez être capable de rendre n'importe quelle action comme fichier pickled, vous avez besoin de sauver le fichier ci-dessus avec le nom "generic.pickle".

Ce ne sont pas tous les objets qui sont pickleable, et tous les objets pickled ne peuvent pas être un-pickled. Il est plus sûr de se contenir à des objets primitifs Python et leurs combinaisons. Les objets qui ne contiennent pas de référence vers des flux de fichier ou des connexions de base de données sont généralement pickleable, mais ils peuvent uniquement être un-pickled dans un environnement où les classes de tous les objets pickled sont déjà définis.

RSS

RSS

web2py inclut une vue "generic.rss" qui peut rendre le dictionnaire retourné par l'action comme flux RSS.

Puisque les flux RSS ont une structure fixée (titre, lien, description, items, etc...) alors pour que cela fonctionne, le dictionnaire retourné par l'action doit avoir la bonne structure :

{'title'      : '',
 'link'       : '',
 'description': '',
 'created_on' : '',
 'entries'    : []}

et chaque entrée dans les entrées doit avoir la même structure similaire :

{'title'      : '',
 'link'       : '',
 'description': '',
 'created_on' : ''}

Par exemple, l'action suivante peut être rendue comme un flux RSS :

def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=[
                  dict(title="my feed",
                  link="http://feed.example.com",
                  description="my first feed")
                ])

en visitant simplement l'URL :

http://127.0.0.1:8000/app/default/feed.rss

De manière alternative, supposons le modèle suivant :

db.define_table('rss_entry',
    Field('title'),
    Field('link'),
    Field('created_on','datetime'),
    Field('description'))

l'action suivante peut aussi être rendue comme un flux RSS :

def feed():
    return dict(title="my feed",
                link="http://feed.example.com",
                description="my first feed",
                entries=db().select(db.rss_entry.ALL).as_list())

La méthode as_list() de l'objet Rows convertir les lignes en une liste de dictionnaires.

Si des objets additionnels de dictionnaire sont trouvés avec les noms de clés qui ne sont pas listés explicitement ici, ils sont ignorés.

Voici la vue "generic.rss" fournie par web2py :

{{
try:
   from gluon.serializers import rss
   response.write(rss(response._vars),escape=False)
   response.headers['Content-Type']='application/rss+xml'
except:
   raise HTTP(405,'no rss')
}}

Comme un exemple complémentaire d'application RSS, nous considérons un aggrégateur RSS qui collecte les données depuis les flux "slashdot" et retourne un nouveau flux RSS web2py.

def aggregator():
    import gluon.contrib.feedparser as feedparser
    d = feedparser.parse(
        "http://rss.slashdot.org/Slashdot/slashdot/to")
    return dict(title=d.channel.title,
                link = d.channel.link,
                description = d.channel.description,
                created_on = request.now,
                entries = [
                  dict(title = entry.title,
                  link = entry.link,
                  description = entry.description,
                  created_on = request.now) for entry in d.entries])

Il peut être accessible à :

http://127.0.0.1:8000/app/default/aggregator.rss

CSV

CSV

Le format Comma Separated Values (CSV) est un protocole pour représenter les données tabulaires.

Considérons le modèle suivant :

db.define_table('animal',
    Field('species'),
    Field('genus'),
    Field('family'))

et l'action suivante :

def animals():
    animals = db().select(db.animal.ALL)
    return dict(animals=animals)

web2py ne fournit pas un "generic.csv" ; vous devez définir une vue personnalisée "default/animals.csv" qui sérialise les animaux dans le CSV. Voici une implémentation possible :

{{
import cStringIO
stream=cStringIO.StringIO()
animals.export_to_csv_file(stream)
response.headers['Content-Type']='application/vnd.ms-excel'
response.write(stream.getvalue(), escape=False)
}}

Notez que l'on pourrait aussi définir un fichier "generic.csv", mais l'on devrait spécifier le nom de l'objet à sérialiser ("animals" dans l'exemple). C'est pourquoi nous ne fournissons pas un fichier "generic.csv".

Remote procedure calls

RPC

web2py fournit un mécanisme pour rendre n'importe quelle fonction en web service. Le mécanisme décrit ici difère du mécanisme décrit avant car :

  • La fonction peut prendre des arguments
  • La fonction peut être définie dans un modèle ou un module au lieu d'un contrôleur
  • Vous pouvez vouloir spécifier en détail quelle méthode RPC devrait être supportée
  • Cela force une convention de nommage plus stricte
  • C'est plus intelligent que les méthodes précédentes puisqu'ils fonctionnent pour un ensemble fixé de protocoles. Pour la même raison, il n'est pas aussi facilement extensible.

Pour utiliser cette fonctionnalité :

Vous devez d'abord importer et initier un objet Service.

from gluon.tools import Service
service = Service()

Ceci est déjà fait dans le fichier de modèle "db.py" dans l'application de référence.

Deuxièmement, vous devez exposer le gestionnaire de service dans le contrôleur :

def call():
    session.forget()
    return service()

Ceci est déjà fait dans le contrôleur "default.py" de l'application de référence. Supprimez session.forget() si vous penser utiliser des cookies de session avec les service.

Troisièmement, vous devez décorer ces fonctions que vous voulez exposer comme un service. Voici une liste de décorateurs actuellement supportés :

@service.run
@service.xml
@service.json
@service.rss
@service.csv
@service.xmlrpc
@service.jsonrpc
@service.jsonrpc2
@service.amfrpc3('domain')
@service.soap('FunctionName',returns={'result':type},args={'param1':type,})

Comme exemple, considérons la fonction décorée suivante :

@service.run
def concat(a,b):
    return a+b

Cette fonction peut être définie dans un modèle ou dans un contrôleur où l'action call est définie. Cette fonction peut maintenant être appelée à distance de deux manières :

http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/run/concat/hello/world

Dans les deux cas, la requête http retourne :

helloworld

Si le décorateur @service.xml est utilisé, la fonction peut être appelée via :

http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/xml/concat/hello/world

et la sortie est retournée comme XML :

<document>
   <result>helloworld</result>
</document>

Il peut sérialiser la sortie de la fonction même si c'est un objet DAL Rows. Dans ce cas, en fait, il appellera as_list() automatiquement.

Si le décorateur @service.json est utilisé, la fonction peut être appelée via :

http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/json/concat/hello/world

et la sortie retournée comme JSON.

Si le décorateur @service.csv est utilisé, le gestionnaire de service requiert, comme valeur de retour, un objet itérable d'objets itérables, telle qu'une liste de listes. Voici un exemple :

@service.csv
def table1(a,b):
    return [[a,b],[1,2]]

Ce service peut être appelé en visitant l'une des URLs suivantes :

http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world
http://127.0.0.1:8000/app/default/call/csv/table1/hello/world

et retourne :

hello,world
1,2

Le décorateur @service.rss attend une valeur de retour dans le même format que la vue "generic.rss" présentée dans la section précédente.

De multiples décorateurs sont autorisés pour chaque fonction.

Jusque là, tout ce qui est présenté dans cette section est simplement une alternative à la méthode décrite dans la section précédente. Le vrai pouvoir de l'objet service vient avec XMLRPC, JSONPRC, et AMFRPC, comme présenté ci-après.

XMLRPC

XMLRPC

Considérons le code suivant, par exemple, dans le contrôleur "default.py" :

@service.xmlrpc
def add(a,b):
    return a+b

@service.xmlrpc
def div(a,b):
    return a/b

Maintenant, dans un shell python vous pouvez faire

>>> from xmlrpclib import ServerProxy
>>> server = ServerProxy(
       'http://127.0.0.1:8000/app/default/call/xmlrpc')
>>> print server.add(3,4)
7
>>> print server.add('hello','world')
'helloworld'
>>> print server.div(12,4)
3
>>> print server.div(1,0)
ZeroDivisionError: integer division or modulo by zero

Le module Python xmlrpclib fournit un client pour le protocole XMLRPC. web2py agit comme le serveur.

Le client se connecte au serveur via ServerProxu et peut appeler à distance les fonctions décorées dans le serveur. Les données (a,v) sont passées à la(les) fonction(s), non pas via des variables GET/POST, mais proprement encodées dans le corps de la requête en utilisant le protocole XMLRPC, et donc s'occupe de son propre type d'information (int ou string ou autre). De même pour la(les) valeur(s) retournée(s). De plus, toute exception levée sur le serveur se propage en retour jusqu'au client.

Il y a des librairies XMLRPC pour de nombreux langages de programmation (incluant C, C++, Java, C#, Ruby, et Perl), et ils peuvent interopérer les uns avec les autres. C'est l'une des meilleures méthodes pour créer des application qui parlent entre elles indépendamment du langage de programmation.

Le client XMLRPC peut alors être implémenté dans une action web2py, afin qu'une action puisse parler à une autre application web2py (même dans la même installation) en utilisant XMLRPC. Attention aux verrous de session dans ce cas. Si une action appelle une fonction via XMLRPC dans la même application, l'appeleur doit relâcher le verrou de session avant l'appel :

session.forget(response)

JSONRPC

JSONRPC

Dans cette section nous allons utiliser le même exemple de code que pour XMLRPC mais nous allons exposer le service en utilisant JSONRPC à la place :

@service.jsonrpc
@service.jsonrpc2
def add(a,b):
    return a+b

def call():
    return service()

JSONRPC est très similaire à XMLRPC mais utilise JSON à la place d'XML comme protocole de sérialisation de données.

Bien sûr nous pouvons appeler le service depuis n'importe quel programme dans n'importe quel langage mais ici nous le ferons en Python. web2py est livré avec un module "gluon/contrib/simplejsonrpc.py" créé par Mariano Reingart. Voici un exemple de comment l'utiliser pour appeler le service ci-dessus :

>>> from gluon.contrib.simplejsonrpc import ServerProxy
>>> URL = "http://127.0.0.1:8000/app/default/call/jsonrpc"
>>> service = ServerProxy(URL, verbose=True)
>>> print service.add(1, 2)

Utilisez "http://127.0.0.1:8000/app/default/call/jsonrpc2" pour jsonrpc2.

JSONRPC et Pyjamas

JSONRPC
Pyjamas

Comme exemple d'application ici, nous présentons l'usage de JSON Remote Procedure Calls avec Pyjamas. Pyjamas est un portage Python du Google Web Toolkit (écrit en Java à la base). Pyjamas permet d'écrire une application client en Python. Pyjamas traduit ce code en JavaScript. Web2py gère le JavaScript et communique avec via des requêtes AJAX depuis le client et déclenchées par des actions utilisateur.

Nous décrivons ici comment faire fonctionner Pyjamas avec web2py. Aucune librairie complémentaire n'est nécessaire que celles de web2py et Pyjamas.

Nous allons construire une simple application "todo" avec un client Pyjamas (tout en JavaScript) qui parle au serveur exclusivement via JSONRPC.

En premier, créons une nouvelle application appelée "todo".

Deuxièmement, dans "models/db.py", entrez le code suivant :

db=DAL('sqlite://storage.sqlite')
db.define_table('todo', Field('task'))
service = Service()

(Note: la classe Service est dans gluon.tools).

Troisièmement, dans "controllers/default.py", entrez le code suivant :

    def index():
    redirect(URL('todoApp'))

    @service.jsonrpc
    def getTasks():
        todos = db(db.todo).select()
        return [(todo.task,todo.id) for todo in todos]

    @service.jsonrpc
    def addTask(taskFromJson):
        db.todo.insert(task= taskFromJson)
        return getTasks()

    @service.jsonrpc
    def deleteTask (idFromJson):
        del db.todo[idFromJson]
        return getTasks()

    def call():
        session.forget()
        return service()

    def todoApp():
        return dict()

Le but de chaque fonction devrait être évident.

Quatrièmement, dans "views/default/todoApp.html", entrez le code suivant :

<html>
  <head>
    <meta name="pygwt:module"
     content="{{=URL('static','output/TodoApp')}}" />
    <title>
      simple todo application
    </title>
  </head>
  <body bgcolor="white">
    <h1>
      simple todo application
    </h1>
    <i>
      type a new task to insert in db,
      click on existing task to delete it
    </i>
    <script language="javascript"
     src="{{=URL('static','output/pygwt.js')}}">
    </script>
  </body>
</html>

Cette vue exécute juste le code Pyjamas dans "static/output/todoapp" - code que nous n'avons pas encore créé.

Cinquièmement, dans "static/TodoApp.py" (notez que c'est TodoApp et non todoApp!), entrez le code client suivant :

from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.TextBox import TextBox
import pyjamas.ui.KeyboardListener
from pyjamas.ui.ListBox import ListBox
from pyjamas.ui.HTML import HTML
from pyjamas.JSONService import JSONProxy

class TodoApp:
    def onModuleLoad(self):
        self.remote = DataService()
        panel = VerticalPanel()

        self.todoTextBox = TextBox()
        self.todoTextBox.addKeyboardListener(self)

        self.todoList = ListBox()
        self.todoList.setVisibleItemCount(7)
        self.todoList.setWidth("200px")
        self.todoList.addClickListener(self)
        self.Status = Label("")

        panel.add(Label("Add New Todo:"))
        panel.add(self.todoTextBox)
        panel.add(Label("Click to Remove:"))
        panel.add(self.todoList)
        panel.add(self.Status)
        self.remote.getTasks(self)

        RootPanel().add(panel)

    def onKeyUp(self, sender, keyCode, modifiers):
        pass

    def onKeyDown(self, sender, keyCode, modifiers):
        pass

    def onKeyPress(self, sender, keyCode, modifiers):
        """
        This function handles the onKeyPress event, and will add the
        item in the text box to the list when the user presses the
        enter key. In the future, this method will also handle the
        auto complete feature.
        """
        if keyCode == KeyboardListener.KEY_ENTER and            sender == self.todoTextBox:
            id = self.remote.addTask(sender.getText(),self)
            sender.setText("")
            if id<0:
                RootPanel().add(HTML("Server Error or Invalid Response"))

    def onClick(self, sender):
        id = self.remote.deleteTask(
                sender.getValue(sender.getSelectedIndex()),self)
        if id<0:
            RootPanel().add(
                HTML("Server Error or Invalid Response"))

    def onRemoteResponse(self, response, request_info):
        self.todoList.clear()
        for task in response:
            self.todoList.addItem(task[0])
            self.todoList.setValue(self.todoList.getItemCount()-1,
                                   task[1])

    def onRemoteError(self, code, message, request_info):
        self.Status.setText("Server Error or Invalid Response: "                             + "ERROR " + code + " - " + message)

class DataService(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "../../default/call/jsonrpc",
                           ["getTasks", "addTask","deleteTask"])

if __name__ == '__main__':
    app = TodoApp()
    app.onModuleLoad()

Sixièmement, démarrez Pyjamais avant de servir l'application :

cd /path/to/todo/static/
python /python/pyjamas-0.5p1/bin/pyjsbuild TodoApp.py

Ceci traduira le code Python en JavaScript afin qu'il puisse être exécuté dans le navigateur.

Pour accéder à l'application, visitez l'URL :

http://127.0.0.1:8000/todo/default/todoApp

Cettte sous-section a été créée par Chris Prinos avec l'aide de Luke Kenneth Casson Leighton (créateurs de Pyjamas), mise à jour par Alexei Vinidiktov. Ceci a été testé avec Pyjamas 0.5p1. L'exemple s'est inspiré de cette page Django en référence [blogspot1].

AMFRPC

PyAMF
Adobe Flash

AMFRPC est le protocole Remote Procedure Call utilisé par les clients Flash pour communiquer avec un serveur. web2py supporte AMFRPC, mais nécessite que vous lanciez web2py depuis les sources et que vous ayez installé au préalable la librairie PyAMF. Celle-ci peut être installée depuis un shell Linux ou Windows en tapant :

easy_install pyamf

(merci de consulter la documentation de PyAMF pour plus de détails).

Dans cette sous-section, nous supposons que vous êtes déjà familier avec la programmation ActionScript.

Nous allons créer un simple service qui prend deux valeurs numériques, les ajoute, et retourne la somme. Nous appellerons notre application web2py "pyamf_test", et nous appellerons le service addNumbers.

Premièrement, utiliser Adobe Flash (n'importe quelle version après MX 2004), créez l'application cliente Flash en démarrant avec un nouveau fichier Flash FLA. Dans la première frame du fichier, ajoutez ces lignes :

import mx.remoting.Service;
import mx.rpc.RelayResponder;
import mx.rpc.FaultEvent;
import mx.rpc.ResultEvent;
import mx.remoting.PendingCall;

var val1 = 23;
var val2 = 86;

service = new Service(
    "http://127.0.0.1:8000/pyamf_test/default/call/amfrpc3",
    null, "mydomain", null, null);

var pc:PendingCall = service.addNumbers(val1, val2);
pc.responder = new RelayResponder(this, "onResult", "onFault");

function onResult(re:ResultEvent):Void {
    trace("Result : " + re.result);
    txt_result.text = re.result;
}

function onFault(fault:FaultEvent):Void {
    trace("Fault: " + fault.fault.faultstring);
}

stop();

Ce code permet au client Flash de se connecter à un service qui correspond à une fonction appelée "addNumbers" dans le fichier "/pyamf_test/default/gateway". Vous devez également importer les classe ActionsScript version 2 de MX pour permettre le Remoting dans Flash. Ajoutez le chemin de ces classes aux paramètres de classpath dans l'IDE Adobe Flash, ou placez simplement le dossier "mx" à côte du nouveau fichier créé.

Notez les arguments du constructeur de Service. Le premier argument est l'URL correspondante au service que nous voulons créer. Le troisième argument est le domaine du service. Nous choisissons d'appeler ce domaine "mydomain".

Deuxièmement, créer un champ texte dynamique appelé "txt_result" et mettez le en avant.

Troisièmement, vous avez besoin de définir une passerelle web2py qui puisse communiquer avec le client Flash définit ci-dessus.

Procédez en créant une nouvelle application appelée pyamf_test qui hébregera les nouveau service et la passerelle AMF pour le client Flash. Editez le contrôleur "default.py" et assurez-vous qu'il contienne

@service.amfrpc3('mydomain')
def addNumbers(val1, val2):
    return val1 + val2

def call(): return service()

Quatrièmement, compilez et exportez/publiez le client Flash SWF comme pyamf_test.swf, placez les fichiers "pyamf_test.amf", "pyamf_test.html", "AC_RunActiveContent.js", et "crossdomain.xml" dans le répertoire "static" de la nouvelle appliance créée qui héberge la passerelle, "pyamf_test".

Vous pouvez maintenant tester le client en visitant :

http://127.0.0.1:8000/pyamf_test/static/pyamf_test.html

La passerelle est appelée en arrière-plan lorsque le client se connecte à addNumbers.

Si vous utiliser AMF0 au lieu de AMF3, vous pouvez aussi utiliser le décorateur :

@service.amfrpc

au lieu de :

@service.amfrpc3('mydomain')

Dans ce cas, vous avez aussi besoin de changer l'URL du service en :

http://127.0.0.1:8000/pyamf_test/default/call/amfrpc

SOAP

SOAP

web2py inclut un client et un serveur SOAP créés par Mariano Reingart. Il peut être utilisé exactement comme XML-RPC :

Considérons le code suivant, par exemple, dans le contrôleur "default.py" :

@service.soap('MyAdd',returns={'result':int},args={'a':int,'b':int,})
def add(a,b):
    return a+b

Maintenant, dans un shell Python vous pouvez faire :

>>> from gluon.contrib.pysimplesoap.client import SoapClient
>>> client = SoapClient(wsdl="http://localhost:8000/app/default/call/soap?WSDL")
>>> print client.MyAdd(a=1,b=2)
{'result': 3}

Pour obtenir le bon encodage lorsque vous retournez des valeurs textes, spécifiez-le dans votre chaîne comme u'proper utf8 text'.

Vous pouvez obtenir le WSDL pour le service avec

http://127.0.0.1:8000/app/default/call/soap?WSDL

Et vous pouvez obtenir la documentation pour n'importe quelle méthode exposée :

http://127.0.0.1:8000/app/default/call/soap

API bas niveau et autres possibilités

simplejson

JSON
simplejson

web2py inclut gluon.contrib.simplejson, développé par Bob Ippolito. Ce module fournit le codeur-décodeur JSON le plus standard en Python.

SimpleJSON consiste en deux fonctions :

  • gluon.contrib.simplesjson.dumps(a) encode un objet Python a en JSON.
  • gluon.contrib.simplejson.loads(b) decode les données JSON b en un objet Python.

Les types d'objet qui peuvent être sérialisés incluent les types primitifs, les listes, et les dicitonnaires. Les objets composés peuvent être sérialisés à l'exception des classes définies par l'utilisateur.

Voici une action exemple (par exemple dans le contrôleur "default.py") qui sérialiser la liste Python contenant les jours de la semaine en utilisant cette API bas-niveau :

def weekdays():
    names=['Sunday','Monday','Tuesday','Wednesday',
           'Thursday','Friday','Saturday']
    import gluon.contrib.simplejson
    return gluon.contrib.simplejson.dumps(names)

Ci-dessous une page HTML qui envoie une requête Ajax à l'action précédente, reçoit le message JSON et stocke la liste dans une variable JavaScript correspondante :

{{extend 'layout.html'}}
<script>
$.getJSON('/application/default/weekdays',
          function(data){ alert(data); });
</script>

Le code utilise la fonction jQuery $.getJSON, qui effectue l'appel Ajax, et lors de la réponse stocke les noms des jours de la semaine dans une variable locale JavaScript data et passe la variable à la fonction de callback. Dans l'exemple, la fonction callback alerte simplement le visiteur que les données ont bien été reçues.

PyRTF

PyRTF
RTF

Un autre besoin commun des sites web est la génération de documents textes lisibles. Le moyen le plus simple de le faire est d'utiliser le format de document Rich Text Format (RTF). Le format a été inventé par Microsoft et est depuis devenu un standard.

web2py inclut gluon.contrib.pyrtf, développé par Simon Cusack et revu par Grant Edwards. Ce module permet de générer des documents RTF de manière programmable, incluant les formatages colorés de texte et les images.

Dans l'exemple suivant, nous initions deux classes basiques RTF, Document et Section, ajoutons le deuxième au premier et insérons du texte aléatoire dans le deuxième :

def makertf():
    import gluon.contrib.pyrtf as q
    doc=q.Document()
    section=q.Section()
    doc.Sections.append(section)
    section.append('Section Title')
    section.append('web2py is great. '*100)
    response.headers['Content-Type']='text/rtf'
    return q.dumps(doc)

A la fin, le Document est sérialisé par q.dumps(doc). Notez qu'avant de retourner un document RTF, il est nécessaire de spécifier le content-type dans l'en-tête sinon le navigateur ne saura pas comment traiter le fichier.

Selon la configuration, le navigateur peut vous demander si vous préférez sauver le fichier ou l'ouvrir en utilisant un éditeur de texte.

ReportLab et PDF

ReportLab
PDF

web2py peut aussi générer des documents PDF, avec une librairie additionnelle appelée "ReportLab"[ReportLab] .

Si vous exécutez web2py depuis les sources, il suffit d'avoir ReportLab installé. Si vous exécutez la distribution binaire Windows, vous avez besoin de décompresser ReportLab dans le dossier "web2py/". Si vous utilisez la distribution binaire Mac, vous avez besoin de décompresser ReportLab dans le répertoire :

web2py.app/Contents/Resources/

A partir de maintenant, nous supposons que ReportLab est installé et que web2py peut le trouver. Nous allons créer une simple action "get_me_a_pdf" qui génère un document PDF.

from reportlab.platypus import *
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib import colors
from uuid import uuid4
from cgi import escape
import os

def get_me_a_pdf():
    title = "This The Doc Title"
    heading = "First Paragraph"
    text = 'bla '* 10000

    styles = getSampleStyleSheet()
    tmpfilename=os.path.join(request.folder,'private',str(uuid4()))
    doc = SimpleDocTemplate(tmpfilename)
    story = []
    story.append(Paragraph(escape(title),styles["Title"]))
    story.append(Paragraph(escape(heading),styles["Heading2"]))
    story.append(Paragraph(escape(text),styles["Normal"]))
    story.append(Spacer(1,2*inch))
    doc.build(story)
    data = open(tmpfilename,"rb").read()
    os.unlink(tmpfilename)
    response.headers['Content-Type']='application/pdf'
    return data

Notez comment nous générons le PDF dans un fichier temporaire unique, tmpfilename, nous lisons le PDF généré depuis ce fichier, ensuite nous le supprimons.

Pour plus d'informations sur l'API ReportLab, référez vous à la documentation ReportLab. Nous vous recommandons très fortement d'utiliser l'API Platypus de ReportLab, telle que Paragraph, Spacer, etc.

Web Services Restful

REST

REST signifie "REpresentational State Transfer" et c'est une type d'architecture de web service et non pas, comme SOAP, un protocole. En fait, il n'y a pas de standard pour REST.

Grosso modo, REST dit qu'un service peut être considéré comme une collection de ressources. Chaque ressource devrait être identifié par une URL. Il y a quatre méthodes d'actions sur une ressource qui sont POST (create), GET (read), PUT (update) et DELETE, pour lesquels l'acronyme CRUD (create-read-update-delete) a sa signification. Un client communique avec la ressource en effectuant une requête HTTP à l'URL qui identifie la ressource et en utilisant la méthode HTTP POST/PUT/GET/DELETE pour passer les instructions à la ressource. L'URL peut avoir une extension, par exemple json qui spécifie comment le protocole peut encoder les données.

Donc par exemple une requête POST à

http://127.0.0.1/myapp/default/api/person

signifie que vous voulez créer une nouvelle person. Dans ce cas, une person peut correspondre à un enregistrement dans la table person mais peut ausi être d'un autre type de ressource (par exemple un fichier).

De la même façon, une requête GET à

http://127.0.0.1/myapp/default/api/persons.json

indique une requête pour obtenir une liste de personnes (enregistrement depuis les données person) au format json.

Une requête GET à

http://127.0.0.1/myapp/default/api/person/1.json

indique une requête pour l'information associée à person/1 (l'enregistrement avec id==1) et au format json.

Dans le cas de web2py, chaque requête peut être coupée en trois parties :

  • Une première partie qui identifie la localisation du service, i.e. l'action qui expose le service :
http://127.0.0.1/myapp/default/api/
  • Le nom de la ressource (person, persons, person/1, etc.)
  • Le protocole de communication spécifié par l'extension

Notez que nous pouvons toujours utiliser le routeur pour éliminer tout préfixe non voulu dans l'URL et par exemple simplifier :

http://127.0.0.1/myapp/default/api/person/1.json

en :

http://127.0.0.1/api/person/1.json

maintenant, c'est une histoire de goût et nous avons déjà présenté cela en long dans le chapitre 4.

Dans notre exemple nous avons utilisé une action appelée api mais ce n'est pas un pré-requis. Nous pouvons en fait nommer l'action qui expose le service RESTful comme on le souhaite et nous pouvons en fait même en créer plus d'un. Pour l'intérêt de l'argument nous continuerons à supposer que notre action RESTful est appelée api.

Nous supposerons aussi que nous avons défini les deux tables suivantes :

db.define_table('person',Field('name'),Field('info'))
db.define_table('pet',Field('owner',db.person),Field('name'),Field('info'))

et elles sont les ressources que nous voulons exposer.

La première chose à faire est de créer l'action RESTful :

def api():
    return locals()

Maintenant nous le modifions afin que l'extension soit filtrée des arguments de la requête (afin que request.args puisse être utilisé pour identifier la ressource) et afin qu'il puisse gérer les différentes méthodes séparément :

@request.restful()
def api():
    def GET(*args,**vars):
        return dict()
    def POST(*args,**vars):
        return dict()
    def PUT(*args,**vars):
        return dict()
    def DELETE(*args,**vars):
        return dict()
    return locals()

Maintenant lorsque nous faisons une requête HTTP GET à

http://127.0.0.1:8000/myapp/default/api/person/1.json

elle appelle et retourne GET('person','1') où GET est la fonction définie dans l'action. Notez que :

  • nous n'avons pas besoin de définir les quatre méthodes, seulement celles que nous souhaitons exposer.
  • la fonction peut prendre des arguments nommés
  • l'extension est stockée dans request.extension et le type de contenu est défini automatiquement.

Le décorateur @request.restful() s'assure que l'extension dans l'information du chemin est stockée dans request.extension, mappe la méthode de requête en la fonction correspondante dans l'action (POST, GET, PUT, DELETE), et passe request.args et request.vars à la fonction sélectionnée.

Maintenant nous construisons un service pour POST et GET des enregistrements individuels :

@request.restful()
def api():
    response.view = 'generic.json'
    def GET(tablename,id):
        if not tablename=='person': raise HTTP(400)
        return dict(person = db.person(id))
    def POST(tablename,**fields):
        if not tablename=='person': raise HTTP(400)
        return db.person.validate_and_insert(**fields)
    return locals()

Notez que :

  • les GET et POST sont traités par des fonctions différentes
  • la fonction s'attend à des arguments corrects (des arguments non nommés parsés par request.args et des arguments nommés depuis request.vars)
  • ils vérifient que l'entrée soit correcte et lève éventuellement une exception
  • GET effectue un select et retourne l'enregistrement, db.person(id). La sortie est automatiquement convertie en JSON puisque la vue générique est appelée.
  • POST effectue un validate_and_insert(..) et retourne l'id du nouvel enregistrement, ou, sinon, les erreurs de validation. Les variables POST, **fields, sont les variables postées.

parse_as_rest (expérimental)

La logique expliquée jusqu'ici est suffisante pour créer n'importe quel type de web service RESTful mais maintenant web2py va même plus loin.

En fait, web2py fournit une syntaxe pour décrire quelles tables de la base de données nous voulons exposer et comment mapper les ressources en URLs et vice-versa.

parse_as_rest

Ceci est fait en utilisant les patterns d'URL. Un pattern est une chaîne qui mappe les arguments de requête depuis l'URL en requête à la base de données. Il y a 4 types de patterns atomiques :

  • Les constantes de chaîne par exemple "friend"
  • La constante de chaîne correspondant à une table. Par exemple "friend[person]" fera correspondre "friends" dans l'URL à la table "person".
  • Les variables à utiliser pour filtrer. Par exemple "{person.id}" appliquera un filtre db.person.name=={person.id}.
  • Les noms de champs représentés par ":field"

Les patterns atomiques peuvent être combinés en des patterns d'URL complexes en utilisant "/" tel que dans

"/friend[person]/{person.id}/:field"

qui donne une URL de la forme

http://..../friend/1/name

en une requête pour une person.id qui retourne le nom de la personne. Ici "friend[person]" matche "friend" et filtre la table "person". "{person.id}" matche "1" et filtre "person.id==1". ":field" matche "name" et retourne :

db(db.person.id==1).select().first().name

De multiples patterns d'URL peuvent etre combinés en une liste afin qu'une seule action RESTful puisse servir différents types de requêtes.

La DAL a une méthode parse_as_rest(pattern,args,vars) qui étant donnée une liste de patterns, les request.args et les request.vars matchent le pattern et retourne une réponse (GET seulement).

Donc voici un exemple plus complexe :

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(*args,**vars):
        patterns = [
            "/friends[person]",
            "/friend/{person.name.startswith}",
            "/friend/{person.name}/:field",
            "/friend/{person.name}/pets[pet.owner]",
            "/friend/{person.name}/pet[pet.owner]/{pet.name}",
            "/friend/{person.name}/pet[pet.owner]/{pet.name}/:field"
            ]
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):
        if table_name == 'person':
            return db.person.validate_and_insert(**vars)
        elif table_name == 'pet':
            return db.pet.validate_and_insert(**vars)
        else:
            raise HTTP(400)
    return locals()

Qui comprend les URLs suivantes qui correspondent aux patterns listés :

  • GET toutes les personnes
http://.../api/friends
  • GET une personne avec le nom commençant par "t"
http://.../api/friend/t
  • GET la valeur du champ "info" de la première personne avec le nom "Tim"
http://.../api/friend/Tim/info
  • GET une liste des animaux de la personne (friend) ci-dessus
http://.../api/friend/Tim/pets
  • GET l'animal avec le nom "Snoopy" de la personne avec le nom "Tim"
http://.../api/friend/Tim/pet/Snoopy
  • GET la valeur du champ "info" pour l'animal
http://.../api/friend/Tim/pet/Snoopy/info

L'action expose également deux urls POST :

  • POST un nouvel ami
  • POST un nouvel animal

Si vous avez l'utilitaire "curl" installé vous pouvez essayer :

$ curl -d "name=Tim" http://127.0.0.1:8000/myapp/default/api/friend.json
{"errors": {}, "id": 1}
$ curl http://127.0.0.1:8000/myapp/default/api/friends.json
{"content": [{"info": null, "name": "Tim", "id": 1}]}
$ curl -d "name=Snoopy&owner=1" http://127.0.0.1:8000/myapp/default/api/pet.json
{"errors": {}, "id": 1}
$ curl http://127.0.0.1:8000/myapp/default/api/friend/Tim/pet/Snoopy.json
{"content": [{"info": null, "owner": 1, "name": "Snoopy", "id": 1}]}

Il est possible de déclarer des requêtes plus complexes telles que l'emplacement d'une valeur dans l'URL utilisée pour construire une requête sans entraîner l'égalité. Par exemple

patterns = ['friends/{person.name.contains}'

mappe

http://..../friends/i

en

db.person.name.contains('i')

Et de même :

patterns = ['friends/{person.name.ge}/{person.name.gt.not}'

mappe

http://..../friends/aa/uu

en

(db.person.name>='aa')&(~(db.person.name>'uu'))

les attributs valides pour un champ dans un pattern sont : contains, startswith, le, ge, lt, gt, eq (equal, default), ne (not equal). Les autres attributs spécifiquement pour les champs date et datetime sont day, month, year, hour, minute, second.

Notez que cette syntaxe de pattern n'est pas destinée à être générale. Toutes les requêtes possibles ne peuvent pas être décrites via un pattern mais beaucoup d'entre elles le sont. La syntaxe peut être étendue dans le futur.

Souvent l'on souhaite exposer quelques URLs RESTful mais l'on souhaite restreindre les requêtes possibles. Ceci peut être fait en passant un argument complémentaire queries à la méthode parse_as_restqueries est un dictionnaire de (tablename,query)query est une requête à la DAL pour restreindre l'accès à la table tablename.

Nous pouvons aussi trier les resultats en utilisant les variables GET

http://..../api/friends?order=name|~info

qui trie par ordre alphabétique (name) et ensuite par ordre inversé info.

Nous pouvons aussi limiter le nombre d'enregistrements en spécifiant des variables GET limit et offset

http://..../api/friends?offset=10&limit=1000

qui retourneront jusqu'à 1000 amis (personnes) et passeront les 10 premiers. limit est par défaut à 1000 et offset par défaut à 0.

Considérons maintenant un cas extrême. Nous voulons construire tous les patterns possibles pour toutes les tables (sauf les tables auth_). Nous voulons être capable de rechercher par n'importe quel champ texte, n'importe quel champ entier, n'importe quel champ double (par rang), et n'importe quelle date (également dans un rang). Nous voulons aussi être capable de POST dans n'importe quelle table :

Dans le cas général, ceci nécessite beaucoup de patterns. Web2py rend cela simple :

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(*args,**vars):
        patterns = 'auto'
        parser = db.parse_as_rest(patterns,args,vars)
        if parser.status == 200:
            return dict(content=parser.response)
        else:
            raise HTTP(parser.status,parser.error)
    def POST(table_name,**vars):
        return db[table_name].validate_and_insert(**vars)
    return locals()

Définir patterns='auto' indique à web2py de générer tous les patterns possibles pour les tables non-auth. Il y a même un pattern pour requêter les patterns :

http://..../api/patterns.json

pour lequel les tables person et pet résultent en :

{"content": [
   "/person[person]",
   "/person/id/{person.id}",
   "/person/id/{person.id}/:field",
   "/person/id/{person.id}/pet[pet.owner]",
   "/person/id/{person.id}/pet[pet.owner]/id/{pet.id}",
   "/person/id/{person.id}/pet[pet.owner]/id/{pet.id}/:field",
   "/person/id/{person.id}/pet[pet.owner]/owner/{pet.owner}",
   "/person/id/{person.id}/pet[pet.owner]/owner/{pet.owner}/:field",
   "/person/name/pet[pet.owner]",
   "/person/name/pet[pet.owner]/id/{pet.id}",
   "/person/name/pet[pet.owner]/id/{pet.id}/:field",
   "/person/name/pet[pet.owner]/owner/{pet.owner}",
   "/person/name/pet[pet.owner]/owner/{pet.owner}/:field",
   "/person/info/pet[pet.owner]",
   "/person/info/pet[pet.owner]/id/{pet.id}",
   "/person/info/pet[pet.owner]/id/{pet.id}/:field",
   "/person/info/pet[pet.owner]/owner/{pet.owner}",
   "/person/info/pet[pet.owner]/owner/{pet.owner}/:field",
   "/pet[pet]",
   "/pet/id/{pet.id}",
   "/pet/id/{pet.id}/:field",
   "/pet/owner/{pet.owner}",
   "/pet/owner/{pet.owner}/:field"
]}

Vous pouvez spécifier les patterns automatique pour certaines tables seulement :

patterns = [':auto[person]',':auto[pet]']

smart_query (experimental)

smart_query

Il y a des fois où vous avez besoin de plus de flexibilité et où vous voulez être capable de passer à un service RESTful une requête comme

http://.../api.json?search=person.name starts with 'T' and person.name contains 'm'

Vous pouvez faire cela en utilisant

@request.restful()
def api():
    response.view = 'generic.'+request.extension
    def GET(search):
        try:
            rows = db.smart_query([db.person,db.pet],search).select()
            return dict(result=rows)
        except RuntimeError:
            raise HTTP(400,"Invalid search string")
    def POST(table_name,**vars):
        return db[table_name].validate_and_insert(**vars)
    return locals()

La méthode db.smart_query prend deux arguments :

  • une liste de champ ou table qui devraient être autorisés dans la requête
  • un chaîne contenant la requête exprimée en langage naturel

et retourne un objet db.set avec les enregistrement qui ont été trouvés.

Notez que la chaîne de recherche est parsée, non évaluée ni exécutée et donc ne permet aucun risque de sécurité.

Contrôle d'accès

L'accès à l'API peut être restreint comme d'habitude en utilisant les décorateurs. Donc, par exemple

auth.settings.allow_basic_login = True

@auth.requires_login()
@request.restful()
def api():
   def GET(s):
       return 'access granted, you said %s' % s
   return locals()

peut maintenant être accédé avec

$ curl --user name:password http://127.0.0.1:8000/myapp/default/api/hello
access granted, you said hello

Services et Authentification

Authentication

Dans le chapitre précédent, nous avons présenté l'usage des décorateurs suivants :

@auth.requires_login()
@auth.requires_membership(...)
@auth.requires_permission(...)

Pour des actions normals (non décorées comme services), ces décorateurs peuvent être utilisés même si la sortie est rendue dans un format autre que HTML.

Pour les fonction définies comme services et décorées en utilisant les décorateurs @service..., les décorateurs @auth... ne devraient pas être utilisés. Les deux types de décorateurs ne peuvent pas être mixés. Si l'authentification doit être effectuée, c'est l'action call qui a besoin d'être décorée :

@auth.requires_login()
def call(): return service()

Notez qu'il est également possible d'instancier de multiples objets service, enregistrer les mêmes fonctions différentes avec elles et en exposer quelques unes avec authentification et d'autres sans :

public_service=Service()
private_service=Service()

@public_service.jsonrpc
@private_service.jsonrpc
def f(): return 'public'

@private_service.jsonrpc
def g(): return 'private'

def public_call(): return public_service()

@auth.requires_login()
def private_call(): return private_service()

Ceci suppose que l'appeleur passe les identifiants dans l'en-tête HTTP (un cookie valide de session ou en utilisant une authentification basic, comme présenté dans la section précédente). Le client doit le supporter ; tous les clients ne le font pas.

 top