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.
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.
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:
À la fin du projet il vous sera demandé de rendre plusieurs choses:
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:
| Groupe | Horaire de passage |
|---|---|
| DENG Feng, LI Ji | 16h |
| JEDLI Mejdi, UEUMRA Satoshi | 16h30 |
| MATTIO Jérémy, KLEIN Michel, NGUYEN Richard | 17h |
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.
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.
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é.
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.
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')>
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'>]
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:
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.
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.
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:
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()
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
Nous avons maintenant les outils bien en main, alors allons-y, écrivons un framework qui nous permettra de faire un blog minimaliste!
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')
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
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)
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
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]
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>
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
Ça marche!

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:
| Brique | Difficulté | Description et commentaires |
| Gestion des cookies | 1 | Vous 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 sessions | 2 | Plutô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 erreurs | 2 | Rattrapez les erreurs éventuelles que votre framework peut rencontrer et gérez les proprement. Ceci peut potentiellement vous aider. |
| Fichiers de logs | 1 | Rajoutez un module qui permet de logguer certaines informations à votre framework. |
| Sauvegarde et restauration de la base de données | 2 (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 statiques | 2 (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. |
| Authentification | 1 (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 fine | 3 | Rajoutez 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 squelette | 2 (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'administration | 3 | Cré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 objets | 2 (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 contenu | 2 | Rajoutez 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 fichiers | 2 (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 fichiers | 2 | Enrichissez l'interface d'administration pour qu'elle puisse gérer le dépôt de fichier. |
| Gestion de cache | 3 | Rajoutez 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 utilisateurs | 2 (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). |
É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).
/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 !