M1. Outils de l'internet, année 2008/2009

Projet - Outils de l'internet

A. Ballier

Le but de ce projet est de réaliser un framework web en python. Il devra former une application WSGI comme vous savez le faire. Il est fortement conseillé d'utiliser des composants libres existants pour grandement alléger votre travail. Ce qui sera évalué est la propreté, l'extensibilité, la sécurité et l'utilisabilité du framework et non pas malheureusement le coté artistique du site web qui l'utilisera.

0.Préliminaires

Dates importantes

Les projets sont à rendre le Lundi 5 Janvier 2009 à 23h59. Seul le screencast est facultatif à cette date.

Les soutenances auront lieu le Lundi 12 Janvier 2009 de 16h à 18h.

Groupes

Avant de commencer, il faut vous répartir en groupes de 2 personnes, avec éventuellement un groupe de 3 si la parité l'impose.

La répartition:

Rendu

À la fin du projet il vous sera demandé de rendre plusieurs choses:

Soutenances

Les soutenances dureront 20 minutes et s'organiseront comme suit:

Les soutenances auront lieu le Lundi 12 Janvier 2009 et les horaires de passage sont les suivants:

GroupeHoraire de passage
DENG Feng, LI Ji16h
JEDLI Mejdi, UEUMRA Satoshi16h30
MATTIO Jérémy, KLEIN Michel, NGUYEN Richard17h

1. Tutoriel

Avant de commencer le développement du projet proprement dit, nous vous proposons ici de vous familiariser avec le concept par le biais de la réalisation d'un squelette de framework minimaliste. Vous pouvez traiter cette partie comme un TP préparatoire. Vous obtiendrez alors à la fin de cette partie une base minimale à partir de laquelle bâtir votre projet. Cette introduction s'inspire très fortement de l'article Why so many Python web frameworks? de J. Gregorio.

Installation

Pour ne pas réinventer la roue vous aurez certainement envie de réutiliser des bouts de code existants ou des "librairies" entières. La solution la plus simple est de télécharger le code source et de l'installer dans votre civet.

Installer un module python est très simple: téléchargez le code source, copiez les fichiers .py nécessaires dans un répertoire de votre choix et rajoutez ce répertoire à votre variable d'environnement PYTHONPATH. Voici un exemple minimaliste:

/tmp/b $ cat /tmp/a/foo.py
def a():
    print "toto"
/tmp/b $ export PYTHONPATH="$PYTHONPATH:/tmp/a"
/tmp/b $ python
...
>>> from foo import a
>>> a()
toto

Et voilà. Vous allez maintenant découvrir les mystérieux paquets à télécharger.

Structure

Votre squelette aura au moins trois modules (et plus si affinités):

Nous allons revenir sur ces points plus en détail. À noter que le choix de WSGI et python sont imposés pour des raisons de portabilité.

Base de données

Vous êtes autorisés à utiliser une ou plusieurs de ces trois types de base de données:

Une dernière méthode optionnelle serait d'utiliser OpenLDAP pour stocker vos utilisateurs, nous y reviendrons plus tard.

Pour utiliser ces bases de données vous avez plusieurs choix:

Nous ne reviendrons pas sur le premier point, les deux derniers allégeront certainement votre travail si vous arrivez à les prendre en main facilement car ils permettent d'abstraire les accès aux base de données au niveau des objets python.

SQLAlchemy

Vous êtes fortement conviés à lire la documentation qui est très complète. Pour vous aider, voici un petit exemple provenant de la documentation que vous devrez adapter à vos besoins:

>>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///:memory:', echo=True)
>>> from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
>>> metadata = MetaData()
>>> users_table = Table('users', metadata,
... Column('id', Integer, primary_key=True),
... Column('name', String),
... Column('fullname', String),
... Column('password', String)
... )
>>> metadata.create_all(engine) 
(snip) des logs (/snip)
>>> class User(object):
...     def __init__(self, name, fullname, password):
...             self.name = name
...             self.fullname = fullname
...             self.password = password
...     def __repr__(self):
...             return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)
... 
>>> from sqlalchemy.orm import mapper
>>> mapper(User, users_table) 
<sqlalchemy.orm.mapper.Mapper object at 0x7f809fc9eb10>
>>> ed_user = User('ed', 'Ed Jones', 'edspassword')
>>> ed_user.name
'ed'
>>> ed_user.password
'edspassword'
>>> str(ed_user.id)
'None'
>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=engine)
>>> session.add(ed_user)
>>> our_user = session.query(User).filter_by(name='ed').first() 
(snip) des logs (/snip)
>>> our_user
<User('ed','Ed Jones', 'edspassword')>
SQLObject

De même, vous êtes fortement conviés à jeter un oeil à la documentation. Toujours de même, voici un petit exemple tiré de la documentation:

>>> from sqlobject import *
>>> sqlhub.processConnection = connectionForURI('sqlite:/:memory:')
>>> class Person(SQLObject):
...     fname = StringCol()
...     lname = StringCol()
...     passwd = StringCol()
... 
>>> Person.createTable()
[]
>>> p = Person(fname="John", lname="Doe", passwd="pass")
>>> p
<Person 1 fname='John' lname='Doe' passwd='pass'>
>>> p2 = Person.get(1)
>>> p2
<Person 1 fname='John' lname='Doe' passwd='pass'>
>>> p is p2
True
>>> p3 = Person(fname="Jack", lname="Rip", passwd="jackpass")
>>> p4 = Person.get(2)
>>> p4
<Person 2 fname='Jack' lname='Rip' passwd='jackpass'>
>>> p5 = Person(fname="John", lname="Rip", passwd="jackpass")
>>> peeps = Person.select(Person.q.fname=="John")
>>> list(peeps)
[<Person 1 fname='John' lname='Doe' passwd='pass'>, <Person 3 fname='John' lname='Rip' passwd='jackpass'>]
>>> peeps = Person.select(Person.q.fname=="Jack")
>>> list(peeps)
[<Person 2 fname='Jack' lname='Rip' passwd='jackpass'>]

Routage

Il est nécessaire que, lorsqu'une page est demandée au serveur, la fonction correspondante soit appelée. Vous avez déjà vu quelques façons de le faire, vous pouvez aussi le faire à la main, mais ici nous allons voir comment utiliser des outils existants qui vont faire la plus grande partie du travail à votre place. Nous allons détailler ici deux façons de faire:

Selector

Vous devrez d'abord télécharger Selector et Resolver.

Reportez vous à la documentation de Selector pour avoir de plus amples informations. Voici, encore une fois, un exemple de code utilisant ce module:

>>> def say_hello(environ, start_response):
...     start_response("200 OK", [('Content-type', 'text/plain')])
...     return ["Hello %s!" %environ['selector.vars']['name']]
... 
>>> from selector import Selector
>>> s = Selector()
>>> s.add('/myapp/hello/{name}', GET=say_hello)
>>> from wsgiref import simple_server
>>> httpd = simple_server.WSGIServer(('',8000),simple_server.WSGIRequestHandler)
>>> httpd.set_app(s)
>>> httpd.serve_forever()

Avec ce petit bout de code, lorsque vous pointerez votre navigateur vers http://localhost:8000/myapp/hello/toto il vous affichera "Hello toto". Vous pouvez maintenant tenter de remplacer toto dans l'URL par autre chose.

Routes

Reportez vous à la documentation ou encore celle à propos de l'intégrer dans un framework web en python. Vous pouvez utiliser le WSGI middleware: routes.middleware.RoutesMiddleware.

Templates

Pour générer votre contenu dynamique vous aurez besoin d'un système de templating qui va, encore une fois, vous simplifier la vie. Il en existe plusieurs, nous allons ici présenter ces trois systèmes:

Kid

Le site contient une bonne documentation ainsi que des exemples. Toutefois, voici un petit bout de code, un fichier t.kid ainsi que le code python permettant de générer ce que l'on veut:

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#">
<body>
<table>
<tr py:for="row in rows">
<td py:content="row">...</td>
</tr>
</table>
</body>
</html>
>>> from kid import Template
>>> def app(environ, start_response):
...     start_response('200 OK',[('Content-type','text/html')])
...     t=Template(file='t.kid', rows=["ligne 1", "ligne 2", "ligne 3"])
...     return [t.serialize()]
...
>>> from wsgiref import simple_server
>>> httpd = simple_server.WSGIServer(('',8000),simple_server.WSGIRequestHandler)
>>> httpd.set_app(app)
>>> httpd.serve_forever()
Cheetah

Idem, reportez-vous à la documentation. Voici encode un exemple constitué du fichier t.tmpl et d'un bout de code python:

#for $a in $rows
row = $a
#end for
>>> from Cheetah.Template import Template
>>> t=Template(file='t.tmpl', searchList=[{'rows': ["ligne 1", "ligne 2", "ligne 3"]}])
>>> print t
row = ligne 1
row = ligne 2
row = ligne 3

Conclusion

Choisissez les outils que vous aller utiliser et faites-le moi savoir.

2. Un framework minimaliste

Nous avons maintenant les outils bien en main, alors allons-y, écrivons un framework qui nous permettra de faire un blog minimaliste!

model.py

Premièrement, un fichier model.py qui se chargera des accès à la base de données. Dans cet exemple nous utiliserons SQLAlchemy.

from sqlalchemy import Table, Column, String
import settings

entry_table = Table('entry', settings.metadata,
             Column('id', String(100), primary_key=True),
             Column('title', String(100)),
             Column('content', String(30000)),
             Column('updated', String(20), index=True)
         )

Ainsi que le fichier de configuration settings.py suivant:

from sqlalchemy import MetaData

metadata = MetaData('sqlite:///blog.db')

manage.py

Maintenant, un fichier qui nous servira à effectuer nos opérations, pour l'instant à créer la base de données:

import os, sys

def createdb():
    from sqlalchemy import Table
    import model
    for (name, table) in vars(model).iteritems():
        if isinstance(table, Table):
            table.create()

if __name__ == "__main__":
   if 'createdb' in sys.argv:
        createdb()

On peut maintenant créer la base de données à l'aide de la commande python manage.py createdb

On peut maintenant rajouter à la main des entrées dans notre base de données:

>>> from model import entry_table
>>> ins=entry_table.insert()
>>> ins.execute(id='entree', title='Mon premier post!', content='Ceci est un post tres interessant', updated='10 Oct 2008')
<sqlalchemy.engine.base.ResultProxy object at 0x7f839bfe6c90>

On peut vérifier que tout s'est bien déroulé:

$ sqlite3 blog.db
...
sqlite>  SELECT * FROM entry;
entree|Mon premier post!|Ceci est un post tres interessant|10 Oct 2008

urls.py

Il nous faut relier des URLs aux fonctions, or nous savons déjà faire:

import selector
import view

urls = selector.Selector()
urls.add('/blog/', GET=view.list)
urls.add('/blog/{id}/', GET=view.member_get)
urls.add('/blog/;create_form', POST=view.create, GET=view.list)
urls.add('/blog/{id}/;edit_form', GET=view.member_get, POST=view.member_update)

views.py

Bien entendu il nous faut les applications WSGI associées à ces URLs:

import renderer
import model

def list(environ, start_response):
    rows = model.entry_table.select().execute()
    return renderer.render(start_response, 'list.html', locals())

def member_get(environ, start_response):
    id = environ['selector.vars']['id']
    row = model.entry_table.select(model.entry_table.c.id==id).execute().fetchone()
    return renderer.render(start_response, 'entry.html', locals())

def create(environ, start_response):
    pass
def create_form(environ, start_response):
    pass
def member_edit_form(environ, start_response):
    pass
def member_update(environ, start_response):
    pass

renderer.py

Et enfin notre module qui se chargera d'afficher nos templates

from kid import Template
import os

def render(start_response, template_file, vars):
    contenttype = "text/html"
    template = Template(file=os.path.join('templates', template_file), **vars)
    body = template.serialize(encoding='utf-8')
    start_response("200 OK", [('Content-Type', contenttype)])
    return [body]

templates/list.html

Puis les sources de notre (nos) template(s).

<?xml version="1.0" encoding="utf-8"?>
<html xmlns:py="http://purl.org/kid/ns#">
<head>
 <title>Mon blog sympa</title>
 </head>
<div py:for="row in rows.fetchall()">
<h2>${row.title}</h2>
<div>${row.content}</div>
<p><a href="./${row.id}/">${row.updated}</a></p>
</div>
</html>

manage.py, bis

Rajoutez et modifiez comme il le faut le fichier manage.py avec le code suivant:

def runserver():
    import urls
    if os.environ.get("REQUEST_METHOD", ""):
        from wsgiref.handlers import BaseCGIHandler
        BaseCGIHandler(sys.stdin, sys.stdout, sys.stderr, os.environ).run(urls.urls)
    else:
        from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
        httpd = WSGIServer(('', 8080), WSGIRequestHandler)
        httpd.set_app(urls.urls)
        print "Serving HTTP on %s port %s ..." % httpd.socket.getsockname()
        httpd.serve_forever()

if __name__ == "__main__":
   if 'createdb' in sys.argv:
        createdb()
   if 'runserver' in sys.argv:
        runserver()

On peut maintenant lancer le serveur: python manage.py runserver

Démonstration

Ça marche!

Screenshot

3. À vous de jouer

Nous avons maintenant un framework minimaliste qui fonctionne, votre travail va consister à l'enrichir selon vos idées et besoins. Comme dit précédemment, il faudra fournir deux applications de votre framework: un blog et une au choix. L'idée étant de vous forcer à écrire un squelette réutilisable dans les deux applications.

Pour vous donner une idée des composant génériques à implémenter, voici une petite table qui tente de synthétiser tout ça; le champ difficulté indique, comme par hasard, la difficulté estimée, mais aussi le fait que plus c'est difficile, plus ça rapporte de points:

BriqueDifficultéDescription et commentaires
Gestion des cookies1Vous devez savoir faire ça, il ne vous reste plus qu'à en faire un MiddleWare WSGI. Vous pouvez utiliser paste comme au TP 4.
Gestion des sessions2Plutôt que de faire comme nous l'avons fait au TP 4 en gardant les identifiants et mots de passe dans des cookies, il est préférable de faire un système de sessions et de stocker l'identifiant de session dans un cookie. Adaptez le décorateur du TP 4 pour qu'il fonctionne ainsi.
Gestion des erreurs2Rattrapez les erreurs éventuelles que votre framework peut rencontrer et gérez les proprement. Ceci peut potentiellement vous aider.
Fichiers de logs1Rajoutez un module qui permet de logguer certaines informations à votre framework.
Sauvegarde et restauration de la base de données2 (ou 4)Rajoutez une commande permettant d'exporter la base de données dans un fichier et de la restaurer à partir de ce fichier. Bonus si vous supportez plusieurs types de base de données et vous pouvez sauvegarder et restaurer en changeant de base de données.
Fichiers statiques2 (ou 3)Fournissez une fonction qui se contentera de servir des fichiers statiques. Notez que vous devez aussi gérer le Content-type grâce à la bibliothèque standard mimetypes et aussi gérer les cas où l'on vous demande des fichiers inexistants. Bonus si vous faites un listing de répertoire lorsqu'on vous le demande.
Authentification1 (ou 3)Un décorateur d'application WSGI qui vérifie que l'utilisateur est bien authentifié avant de servir une page, vous l'avez fait au TP 4. Bonus si vous le faites avec OpenLDAP avec python-ldap.
Authentification fine3Rajoutez un niveau d'autorisation à vos utilisateurs et modifiez le décorateur précédent pour qu'il prenne un paramètre. Inspirez-vous de la PEP
Compléter le squelette2 (ou 4)Maintenant que vous avez un système d'authentification, complétez le squelette du framework précédent pour autoriser certains utilisateurs à rajouter des entrées. Bonus si vous avez un système d'authentification fine et que vous avez plusieurs niveaux d'autorisations (par exemple pour un blog: poster et éditer des billets, rajouter des commentaires, etc.).
Interface d'administration3Créez une interface d'administration à accès restreint depuis laquelle on peut gérer les utilisateurs enregistrés (en rajouter, en effacer, modifier les existants).
Formulaires et tables HTML en objets2 (ou 3)À l'instar de ce que font SQLAlchemy et compagnie, rajoutez un module permettant de créer des formulaires en tant qu'objets python et générant le code HTML correspondant. Bonus si vous rajoutez des vérifications de types lors de la création et de validité du code généré.
Interface d'administration du contenu2Rajoutez la possibilité de parcourir les tables de votre base de données à l'interface d'administration. Vous devrez vous servir des objets permettant de générer des tables HTML.
Dépôt de fichiers2 (ou 3)Rajoutez un module qui permet aux utilisateurs enregistrés de déposer des fichiers et de les récupérer. Bonus si vous gardez une notion de propriété et de droits sur les fichiers pour que les utilisateurs ne puissent pas lire ou écraser les fichiers des autres.
Administration du dépôt de fichiers2Enrichissez l'interface d'administration pour qu'elle puisse gérer le dépôt de fichier.
Gestion de cache3Rajoutez un MiddleWare qui gère un cache et court-circuite la génération des pages lorsqu'il connaît déjà le résultat. Attention à être consistant et à ne pas court-circuiter la génération lorsqu'il ne fallait pas...
Enregistrer les utilisateurs2 (ou 3)Permettre aux utilisateurs de s'enregistrer dans votre base de données via un formulaire HTML (plutôt que d'avoir à les créer à la main...). Bonus si vous faites un système de vérification par mail et/ou de prévention d'enregistrement automatisé (pour éviter les robots).

Contrat

Évidemment il n'est pas demandé d'implémenter tous les modules décrits plus haut, en revanche, il est interdit d'utiliser certains outils tels que Django, Pylons ou TurboGears puisqu'ils font déjà une grande partie de ce qui est demandé. Néanmoins il n'est pas interdit d'aller regarder et de s'inspirer de ce qui est fait.

De manière générale, si vous souhaitez utiliser un composant non listé sur cette page, demandez avant (soit pendant les séances de TP soit par mail).

Faites moi savoir, dès que vous êtes fixés, ce que va être la deuxième application de votre framework

4. Divers

Faire un screencast

/home/ballier/bin/ffmpeg -s 1280x1024 -r 10 -f x11grab -i :0.0 -pix_fmt rgb24 -vcodec qtrle toto.mov

Et voilà, ça enregistre ce qu'il se passe à l'écran !