Rappel
Pyramid utilise un système de routes prédéclarées dans le fichier __init__.py (voir précédent article sur les routes).
Les vues matchent ces routes et renvoient les paramètres aux moteurs de templates qui transforment en html (ou autre chose) et renvoient aux clients.
Les views
Par défaut, le fichier views.py rassemble les différentes vues :
from pyramid.view import view_config
@view_config(route_name='home', renderer='templates/home.pt')
def home(request):
return {'project': 'foo'}
@view_config(route_name='controls', renderer='templates/controls.pt')
def controls(request):
return {'status': 'ok',
'values': [1, 2, 3]}
Les vues peuvent être éclatées dans plusieurs fichiers, peuvent être écrites sous forme de classes.
class ViewSample(object):
def __init__(self, request):
self._request = request
@view_config(route_name='home', renderer='templates/home.pt')
def home(self):
return {'project': 'foo'}
@view_config(route_name='controls', renderer='templates/controls.pt')
def controls(request):
return {'status': 'ok', 'values': [1, 2, 3]}
Une vue est une fonction décorée par view_config. La fonction prends en paramètre un objet request (abordé dans un prochain billet) qui correspond à la requête faite par l'utilisateur. La fonction retourne ou un objet Response (abordé dans un prochain billet : sans doute le même que request) ou objet manipulable par le moteur de rendu. Le décorateur view_config décrit le rendu de la vue et son appel.
@view_config
view_config peut prendre un grand nombre de paramètres :
- accept,
- attr,
- check_csrf
- containment,
- context,
- custom_predicates,
- decorator,
- effective_principals
- header,
- http_cache,
- mapper,
- match_param,
- name,
- path_info,
- permission,
- predicates
- physical_path,
- renderer,
- request_method,
- request_param,
- request_type,
- route_name,
- wrapper,
- xhr.
Soient 24 paramètres possibles... Je ne vais pas tous les détailler : j'ignore l'usage de certains et d'autres sont très pointus. Pour les plus curieux, la documentation de pyramid les détaille. Tous sont optionnels, il n'est donc pas nécéssaire de tous les spécifier sur une méthode.
La documenation de pyramid distingue deux types de paramètres à @view_config : les prédicats et les non prédicats.
Les non prédicats sont permission, attr, renderer, http_cache, wrapper, decorator et mapper. Mécaniquement, tous sont des prédicats.
route_name
route_name décrit quelle route est rendue par cette fonction. route_name corresponds au nom de la route rajouté dans le config.add_route du __init__.py. Plusieurs fonctions peuvent avoir le même route_name (voir plus bas.)
route_name est un prédicat : la fonction décorée par @view_config ne sera appelée que si la requête HTTP match cette route.
from pyramid.view import view_config
@view_config(route_name='home', renderer='json')
def home(request):
return {'func': 'home'}
@view_config(route_name='foo', renderer='json')
def foo(request):
return {'func': 'foo'}
@view_config(renderer='json')
def default(request):
return {'func': 'default'}
Les fonctions home et foo seront appelées quand les routes home et foo seront matchées. défault ne sera jamais appelée. route_name est bien optionnel car il y a d'autre moyen de matcher une fonction mais dans les faits, il est vraiment nécéssaire : les cas où route_name n'est pas utilisé sont un peu plus compliqués et on verra peut être ça une autre fois.
Il est possible d'avoir plusieurs fois le décorateur view_config sur une même fonction.
Imaginons le fichier __init__.py suivant :
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.add_route('home', '/')
config.add_route('foo', '/foo')
config.add_route('baz', '/baz')
config.scan()
return config.make_wsgi_app()
et le fichier views.py suivant :
from pyramid.view import view_config
@view_config(route_name='home', renderer='json')
def home(request):
return {'func': 'home'}
@view_config(route_name='foo', renderer='json')
@view_config(route_name='baz', renderer='json')
def default(request):
return {'func': 'default'}
La méthode default matchera les route foo et baz :
$ curl http://0.0.0.0:6543/ && echo
{"func": "home"}
$ curl http://0.0.0.0:6543/foo && echo
{"func": "default"}
$ curl http://0.0.0.0:6543/baz && echo
{"func": "default"}
renderer
renderer décrit le rendu utilisé lors de l'appel. Par défaut, pyramid est[1] livré avec trois moteurs de rendu : string, json et chameleon. D'autres moteurs sont disponibles tel que mako, genshi, jinja2 ou même un moteur maison.
[1] | était serait plus juste. Depuis la version 1.5 de pyramid, chameleon n'est plus livré par défaut dans pyramid. Cependant, il reste celui utilisé par pcreate pour créer les templates ou exemple d'application. |
Le renderer json transforme la réponse en json ; la réponse doit donc être une chaine de caractères, une liste, un tuple, un dictionnaire ou une combinaison des précédents.
string retourne la représentation str du return.
Différence entre string et json :
from pyramid.view import view_config
@view_config(route_name='fooString', renderer="string")
def fooString(request):
return ["fooString"]
@view_config(route_name='fooJSON', renderer="json")
def fooJson(request):
return ["fooJson"]
Les retours seront :
$ curl -i http://0.0.0.0:6543/foo/string && echo
HTTP/1.1 200 OK
Content-Length: 9
Content-Type: text/plain; charset=UTF-8
Date: Mon, 11 Nov 2013 11:17:25 GMT
Server: waitress
['fooString']
$ curl -i http://0.0.0.0:6543/foo/json && echo
HTTP/1.1 200 OK
Content-Length: 9
Content-Type: application/json; charset=UTF-8
Date: Mon, 11 Nov 2013 11:17:28 GMT
Server: waitress
["fooJson"]
Il est important de noter le changment de Content-Type selon le moteur de rendu.
renderer peut aussi prendre un chemin vers un template tel que templates/foo.pt. l'extention .pt signifie page template et indique que le moteur de rendu ici est chameleon. Les rendus chameleon prennent en paramètre des dictionnaires pouvant contenir des objets. Content-Type sera du HTML
Si le paramètre renderer est absent, il faut renvoyer un objet Response.
from pyramid.view import view_config
@view_config(route_name='baz')
def baz(request):
request.response.body = 'baz'
return request.response
Et la réponse renvoyée au client ne contiendra que baz :
$ curl http://0.0.0.0:6543/baz && echo
baz
Request et Response feront l'oject d'un prochain billet (vraiment faut que je le fasse). Comme dans un prochain billet, je montrerais comment rajouter un moteur de rendu à pyramid.
Pyramid peut avoir plusieurs moteurs de rendu simultanément : certaines fonctions peuvent utiliser mako et d'autres chameleon ou json.
request_method
Le paramètre corresponds request_method aux verbes HTTP utilisés pour acceder à la page :
- GET,
- POST,
- PUT,
- DELETE,
- HEAD.
NB : Les exemples suivant sont en JSON avec curl, ils s'appliquent tous aussi bien avec un rendu HTML et un navigateur. curl et JSON sont juste ici pour la facilité d'écriture.
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', request_method='GET')
def get(request):
return 'GET'
@view_config(route_name='baz', renderer='json', request_method='POST')
def post(request):
return 'POST'
@view_config(route_name='baz', renderer='json', request_method='HEAD')
def head(request):
return 'HEAD'
@view_config(route_name='baz', renderer='json', request_method='PUT')
def put(request):
return 'PUT'
@view_config(route_name='baz', renderer='json', request_method='DELETE')
def delete(request):
return 'DELETE'
Ce qui donnera avec curl:
$ curl -XGET http://0.0.0.0:6543/baz && echo
"GET"
$ curl -XPOST http://0.0.0.0:6543/baz && echo
"POST"
$ curl -XHEAD http://0.0.0.0:6543/baz && echo
curl: (18) transfer closed with 5 bytes remaining to read
$ curl -XPUT http://0.0.0.0:6543/baz && echo
"PUT"
$ curl -XDELETE http://0.0.0.0:6543/baz && echo
"DELETE"
Pour HEAD, c'est un non sens de renvoyer une valeur dans le body d'où l'erreur de curl.
request_method peut être également une liste de méthode acceptées :
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', request_method='GET')
def get(request):
return 'GET'
@view_config(route_name='baz', renderer='json', request_method=['POST', 'PUT'])
def post(request):
return 'POST or PUT'
$ curl -XGET http://0.0.0.0:6543/baz && echo
"GET"
$ curl -XPOST http://0.0.0.0:6543/baz && echo
"POST or PUT"
$ curl -XPUT http://0.0.0.0:6543/baz && echo
"POST or PUT"
On aurait pu également écrire:
@view_config(route_name='baz', renderer='json', request_method='POST')
@view_config(route_name='baz', renderer='json', request_method='PUT')
def post(request):
return 'POST or PUT'
Sans le paramètre request_method, la vue s'écrirait :
@view_config(route_name='baz', renderer='json')
def baz(request):
if request.method in ['POST', 'PUT']:
return 'POST or PUT'
elif request.method == 'GET':
return 'GET'
return '???' #TODO: ici généré une 404
$ curl -XGET http://0.0.0.0:6543/baz && echo
"GET"
$ curl -XPUT http://0.0.0.0:6543/baz && echo
"POST or PUT"
$ curl -XPOST http://0.0.0.0:6543/baz && echo
"POST or PUT"
$ curl -XDELETE http://0.0.0.0:6543/baz && echo
"???"
Si on ne définit pas toutes les request_method, certains verbes vont générer des 404 (ce qui peut être un comportement légitime).
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', request_method='GET')
def get(request):
return 'GET'
@view_config(route_name='baz', renderer='json', request_method='POST')
def post(request):
return 'POST'
$ curl -XGET http://0.0.0.0:6543/baz && echo
"GET"
$ curl -XPOST http://0.0.0.0:6543/baz && echo
"POST"
$curl -XPUT -v http://0.0.0.0:6543/baz >/dev/null && echo
* About to connect() to 0.0.0.0 port 6543 (#0)
* Trying 0.0.0.0...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to 0.0.0.0 (0.0.0.0) port 6543 (#0)
> PUT /baz HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 0.0.0.0:6543
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Length: 61026
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 11 Nov 2013 11:36:21 GMT
< Server: waitress
<
{ [data not shown]
100 61026 100 61026 0 0 2436k 0 --:--:-- --:--:-- --:--:-- 2483k
* Connection #0 to host 0.0.0.0 left intact
Dans l'absolu, on peut utiliser ses propres verbes même si cela me parait une mauvaise idée[2] :
@view_config(route_name='baz', renderer='json', request_method='FOO')
def foo(request):
return 'FOO'
$ curl -XFOO http://0.0.0.0:6543/baz && echo
"FOO"
[2] | sans compter qu'il faut que les reverses proxy, les serveurs WSGI acceptent ces nouveaux verbes. Dans une optique webservice REST, les verbes webdav, par contre, peuvent être utiles. |
Pour faire un webservice REST, cornice est une bonne surcouche à pyramid qui permet de gagner du temps.
Pour finir, pyramid 1.5 a introduit not_ qui est utilisable dans request_method :
from pyramid.view import view_config
from pyramid.config import not_
@view_config(route_name='baz', renderer='json', request_method=not_('POST'))
def post(request):
return 'not POST'
$ curl -XGET http://0.0.0.0:6543/baz && echo
"not POST"
$ curl -XPUT http://0.0.0.0:6543/baz && echo
"not POST"
$ curl -XPOST -v http://0.0.0.0:6543/baz > /dev/null && echo
* About to connect() to 0.0.0.0 port 6543 (#0)
* Trying 0.0.0.0...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to 0.0.0.0 (0.0.0.0) port 6543 (#0)
> POST /baz HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 0.0.0.0:6543
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Length: 61416
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 11 Nov 2013 12:04:00 GMT
< Server: waitress
<
{ [data not shown]
100 61416 100 61416 0 0 4355k 0 --:--:-- --:--:-- --:--:-- 4613k
* Connection #0 to host 0.0.0.0 left intact
accept
- accept liste les types mimes de réponses acceptées par le client :
- application/json, application/xml, text/html...
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', accept='application/json')
def baz(request):
return 'baz'
@view_config(route_name='baz', renderer='string', accept='application/xml')
def bazxml(request):
return '<foo>baz</foo>'
$ curl -H "Accept: application/xml" http://0.0.0.0:6543/baz && echo
<foo>baz</foo>
$ curl -H "Accept: application/json" http://0.0.0.0:6543/baz && echo
"baz"
Attention ! Cela ne se spécifie pas le content type de la réponse ! il faut le faire explicitement ou via le moteur de rendu.
@view_config(route_name='baz', renderer='string', accept='application/xml')
def bazxml(request):
request.response.content_type = 'application/xml'
return '<foo>baz</foo>'
Le client peut passer un Accept sous la forme d'un mimetype ou sous la forme text/* ou */*.
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', accept='application/json')
def baz(request):
return 'baz'
@view_config(route_name='baz', renderer='string', accept='text/xml')
def bazxml(request):
return '<foo>baz</foo>'
$ curl -H "Accept: application/json" http://0.0.0.0:6543/baz && echo
"baz"
$ curl -H "Accept: application/*" http://0.0.0.0:6543/baz && echo
"baz"
$ curl -H "Accept: text/*" http://0.0.0.0:6543/baz && echo
<foo>baz</foo>
$ curl -H "Accept: */*" http://0.0.0.0:6543/baz && echo
"baz"
$ curl -H "Accept: */*" http://0.0.0.0:6543/baz && echo
Le premier accept qui match est retourné. Si accept est absent, il n'y a pas de discrimination sur ce header HTTP.
header
header contient un entête HTTP qui doit matcher :
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', header='X-custom-header:bar')
def bar(request):
return 'bar'
@view_config(route_name='baz', renderer='json', header='X-custom-header:foo')
def foo(request):
return 'foo'
$ curl -H "X-custom-header:bar" http://0.0.0.0:6543/baz && echo
"bar"
$ curl -H "X-custom-header:foo" http://0.0.0.0:6543/baz && echo
"foo"
Je n'ai jamais utilisé ce paramètre et je n'imagine pas de cas d'usage pertinent[3]. Dans un usage d'un webservice, pour faire la différence entre un client android et un autre iOS ? La documentation donne un exemple avec le user-agent... Si quelqu'un a une idée cas d'usage pertinent, je suis preneur.
[3] | mes autres exemples étaient quand même vaguement pertinents, non ? |
http_cache
http_cache positionne dans les headers de la réponse la durée du cache coté client. La valeur du paramètre peut être un entier positif ou nul qui est le temps en secondes, un timedelta ou un tuple donc le premier paramètre est un entier ou un timedelta et le second un dictionnaire sur la politique du cache.
from datetime import timedelta
from pyramid.view import view_config
@view_config(route_name='baz', renderer='json', http_cache=2)
def baz(request):
return 'baz'
@view_config(route_name='foo', renderer='json', http_cache=timedelta(days=1))
def foo(request):
return 'foo'
@view_config(route_name='bar', renderer='json', http_cache=(4, {'public': True}))
def bar(request):
return 'bar'
"baz"
$ curl -i http://0.0.0.0:6543/foo && echo
HTTP/1.1 200 OK
Cache-Control: max-age=86400
Content-Length: 5
Content-Type: application/json; charset=UTF-8
Date: Mon, 11 Nov 2013 13:47:14 GMT
Expires: Tue, 12 Nov 2013 13:47:14 GMT
Server: waitress
"foo"
$ curl -i http://0.0.0.0:6543/bar && echo
HTTP/1.1 200 OK
Cache-Control: max-age=4, public
Content-Length: 5
Content-Type: application/json; charset=UTF-8
Date: Mon, 11 Nov 2013 13:52:55 GMT
Expires: Mon, 11 Nov 2013 13:52:59 GMT
Server: waitress
"bar"
match_param
match_param est un prédicat sur le contenu du path.
Soit le fichier __init__.py suivant :
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.add_route('foo', '/foo/{baz}')
config.scan()
return config.make_wsgi_app()
et le fichier views correspondant :
from pyramid.view import view_config
@view_config(route_name='foo', renderer='json', match_param='baz=foo')
def foo(request):
return 'foo'
@view_config(route_name='foo', renderer='json', match_param='baz=bar')
def bar(request):
return 'bar'
Ce qui donne lors des appels :
$ curl http://0.0.0.0:6543/foo/bar && echo
"bar"
$ curl http://0.0.0.0:6543/foo/foo && echo
"foo"
permission
permission liste les permissions pour acceder à la vue. Cela rentre dans les authentifications et les permissions de pyramid qui fera l'objet d'un billet dédié.
Combinaison des paramêtres
Après cette longue liste partielle de paramètres, l'intéret est de les combiner pour offrir une granularité fine pour les réponses.
@view_config(name="home", accept='application/json', renderer='json')
def repJson(request):
"""réponse en json"""
return {'foo': 'bar'}
@view_config(name="home", request_method='GET', renderer='templates/home.pt')
def repGet(request):
return {'params': 'foo'}
@view_config(name="home", request_method='POST', renderer='templates/home.pt')
def repPost(request):
return {'params': 'bar'}
@view_config(name="home", request_method='PUT', renderer='templates/homeput.pt')
def repPut(request):
return {'params': 'baz'}
Dans cet exemple, la même route home peut répondre selon les combinaisons de quatre manières différentes en utilisant s'il le faut différents rendus.
Ajout de paramètres
Si la liste ne suffit pas, il est possible de créer ses propres prédicats.
Il n'y a pas d'interface au sens ZCA[4] mais la classe doit respecter la signature suivante :
[4] | yep y aura un billet sur la ZCA et pyramid s'en sert un peu. Ce n'est pas nécessaire pour comprendre les entrailles de pyramid mais ça peut aider. |
class Predicate(object):
def __init__(self, val, config):
pass
def text(self):
return 'une signature'
phash = text
def __call__(self, context, request):
return True # retourne un booléen
Exemple de prédicat sur le jour de la semaine.
class DayPredicate(object):
_choice = {'monday': 0,
'thursday': 1,
'wedesnday': 2,
'thuesday' : 3,
'friday': 4,
'saturday': 5,
'sunday': 6}
def __init__(self, val, config):
self._val = val
self._numVal = self._choice[val]
def text(self):
return 'predicat on %s' % self._val
phash = text
def __call__(self, context, request):
return datetime.datetime.today().weekday() == self._numVal
Ajout du prédicat à pyramid:
config.add_view_predicate('day', DayPredicate)
et enfin l'utilisation.
@view_config(route_name='foo', renderer='json', day='monday')
def monday(request):
return 'monday'
@view_config(route_name='foo', renderer=json', day='sunday')
def sunday(request):
return 'sunday'
Ce qui donnera :
$ curl http://0.0.0.0:6543/foo && echo
"monday"
Une autre approche est d'utiliser le paramètre custom_predicate de view_config.
@view_config(route_name='foo', renderer='json', custom_predicate=(DayPredicate('monday', None), ))
def monday(request):
return 'monday'
@view_config(route_name='foo', renderer='json', custom_predicate=(DayPredicate('sunday', None), ))
def sunday(request):
return 'sunday'
Des usages plus pertinents seraient des prédicats logged ou admin pour filtrer si l'utilisateur est loggé ou admin.
C'est tout pour aujourd'hui
Le décorateur @view_config avec sa granularité et son extensibilité est une des choses que j'aime beaucoup dans pyramid.