Le décorateur view_config de pyramid

Le dans «fr python» par Cyprien Le Pannérer
Mots-clés:

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.

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.