Quelques avantages de Docker

published on April 5, 2017, 7:53 a.m. by eliotberriot | 0

Suite à une discussion avec @Exagone313 je me suis rendu compet qu'il n'était pas toujours évident de faire ressortir les avantages et les inconvénients de Docker.

Docker c'est quoi ?

Docker c'est une galaxie d'outils permettant de faire tourner des containers (un peu comme des machines virtuelles, mais en beaucoup plus léger, et qui bootent beaucoup plus rapidement).

Il y a le docker-engine, qui fait tourner les containers proprement dits, mais également le Docker Hub, où l'ont peut déployer et partager ses images (des templates de containers), docker-compose pour décrire et lancer des configurations de containers plus complexes etc.

Exemple

Une fois docker installé, mettons que vous vouliez remonter dans le temps sur une Debian 6. Il vous suffit de faire:

docker run -it debian:6 bash

docker run va lancer un container. -it précise que c'est en mode interactif. debian:6 précise l'image et le tag à utiliser. bash est la commande à lancer dans le container en question.

Si l'image debian:6 n'est pas présente sur ma machine, elle sera automatiquement récupérée depuis le Docker Hub (pour peut qu'elle existe). Ensuite, un nouveau container est créé a partir de cette image, et la commande bash est lancé dedans.

J'ai fait bash mais j'aurai pu faire docker run -it debian:6 ls /etc et cela aurait foncionné tout pareil (lister le contenu du dossier /etc dans le container).

À quoi ça sert ?

Si vous ne tenez pas particulièrement à faire tourner une Debian 6, l'exemple précédent ne vous sert à rien. Par contre cela peut servir à tester / déployer de très nombreux outils en une seule commande.

Prenez PostgreSQL par exemple. Si vous voulez tester cet outil sans Docker, vous allez devoir installer le serveur PostgreSQL sur votre machine, ce qui inclut un certain nombre de dépendances. Les paquets à installer peuvent varier selon si vous vous trouvez sur Debian, CentOS, Windows...

Avec Docker, il vous suffit de lancer docker run -d --name test-postgres postgres et vous avez un serveur PostgreSQL qui tourne localement sur votre machine, complêtement isolé de votre réseau. Une fois que vous avez fini vos tests, vous pouvez simplement faire un docker stop test-postgres puis un docker rm test-postgres pour supprimer le container.

Pour des scenarios plus complexes, les gains sont encore plus évident. Le projet sur lequel je travaille au boulot est un projet Django / Python 3.4, qui requiert également Redis, PostgreSQL, ElasticSearch et Celery pour fonctionner. Certains développeurs travaillent sur Debian, d'autres sur Ubuntu, d'autres sur MacOS, ce qui fait qu'il serait lourd, fragile et complexe de créer des scripts d'installation pour chaque cas. D'ailleurs, certains développeurs ont peut être déjà un serveur PostgreSQL ou Redis qui tourne sur leur machine pour d'autres projets, dans des versions différentes.

Avec Docker et docker-compose, il leur suffit de cloner le dépôt Git du projet et de lancer un docker-compose up pour installer l'ensemble des dépendances, et lancer les containers du projet (l'application, le serveur de base de données, de cache, etc.).

On passe d'un setup de plusieurs heures auparavant (installer tout à la main ou de façon semi automatique, configurer les différents services...) à quelques minutes, qui sont simplement consacrées à la récupération des containers et à la configuration du projet en lui même (et pas de ses dépendances).

Maintenant, si un développeur arrête de travailler sur le projet, il lui suffit de faire docker-compose down pour supprimer tous les containers liés au projet.

Bien sûr, il pourrait aussi faire le travail manuellement:

  • Aller dans leur serveur de BDD et supprimer les database du projet
  • Supprimer les clés dans leur cache
  • Désinstaller Celery et Elasticsearch
  • Supprimer Python 3.4 et les paquests installés spécifiquement pour le projet
  • J'en oublie sûrement, et c'est bien le problème

Il est tout simplement beaucoup plus simple et robuste de le faire avec un docker-compose down.

Pour l'open-source, le gain est ainsi énorme puisqu'il est possible de partager très simplement une application et la configuration docker correspondante, permettant des déploiements beaucoup plus rapides et plus simples.

Exemple de configuration Docker-compose

Voici une configuration docker-compose type:

version: '2'

services:
    database:
        image: postgres:9.4.2  # j'ai besoin d'une version très précise

    redis:
        image: redis

    application:
        image: monimageperso
        # la commande exécutée dans le container lors du up
        command: run_the_application_server
        volumes:
            # on monte le code du projet pour que les changements locaux
            # soient reflétés dans le container
            - .:/code

Avec ça, en moins de 20 lignes j'ai une configuration pour une base de données, un cache et mon application, le tout de façon complêtement isolée du reste de mon système: l'application pourra par exemple parler à la base de données, mais celle-ci est invisible pour le reste de mon système.

Si je partage ça avec un autre développeur, cela fonctionnera exactement de la même manière sur sa machine que sur la mienne. Et si un jour, je veux rajouter un quatrième service:

background_tasks:
    image: celery

Je peux le faire très facilement. Tout développeur qui utilisera mon version mise à jour aura le nouveau service de lancé, sans autre chose à faire.

Au delà

Docker est bien sûr beaucoup plus complexe que ça et gère de nombreuses autres fonctionnalités, notamment liées à des déploiement de grande envergure. Docker-swarm, par exemple, permet de déployer des applications à grande échelle (par exemple 50 instance de mon application réparties sur 4 serveurs, et 3 serveurs de cache), de vérifier que les containers sont en bonne santé et de les redémarrer si besoin.

La partie networking est également extrêmement riche, il est ainsi possible de créer à la volée des réseaux spécifiques et d'y rattacher certains containers.

Voilà, j'espère que les avantages d'utiliser Docker sont un peu plus clairs :)

Déployer une instance Mastodon en 5 minutes avec Docker

published on April 4, 2017, 9:09 p.m. by eliotberriot | 0

Vous le savez peut-être, il y a une hype grandissante autour de Mastodon depuis quelques jours / heures.

Si vous ne connaissez pas, Mastodon est une alternative à Twitter libre, open-source et décentralisée : pour rejoindre le réseau, il faut vous inscrire sur une instance (un peu comme les "Pod" de Diaspora*). Une instance peut accueillir un nombre illimité d'utilisateur et communiquer avec d'autres instances.

Certaines instances publiques sont mises à disposition, telles que https://mastodon.xyz ou https://mastodon.social. Cependant, la philosophie derrière ce type d'outils est de déployer sa propre instance lorsque c'est possible pour rendre l'ensemble du réseau plus résistant et plus décentralisé, tout en allégeant la charge sur les instances publiques les plus grosses.

Un tutoriel pas à pas à été publié par @angristan pour installer le tout sur Debian 8, mais si vous êtes comme moi pressé et peu désireux d'encombrer votre système avec des dépendances, des serveurs de base de données, de cache et autres joyeusetés, il est très facile de déployer votre propre instance avec Docker et docker-compose.

Installation d'une instance Mastodon

Je pars du principe que vous avez docker et docker-compose d'installé. La configuration que je vous propose est en reverse proxy, avec Nginx devant Mastodon, afin que vous puissiez servir d'autres sites sur les ports 80 et 443 de votre serveur.

Sur le serveur ou vous souhaitez installer l'instance en question, rendez-vous dans le dossier /srv.

Clonez le dépôt Git de Mastodon:

git clone https://github.com/tootsuite/mastodon.git

Placez vous dans le dossier nouvellement créé:

cd /srv/mastodon

Créez le fichier d'environnement:

cp .env.production.sample .env.production

Lancez le build:

docker-compose build

Créez les secrets nécessaires pour faire fonctionner Mastodon. Il en faut trois, donc exécutez trois fois la commande suivante et conservez les résultats dans un fichier à part:

docker-compose run --rm web rake secret

Éditez le fichier d'environnement:

nano .env.production

Éditez-le en fournissant les valeurs adéquates. Le fichier est globalement bien commenté en lui même, les valeurs indispensables aux fonctionnement de l'instance sont:

LOCAL_DOMAIN=example.com -> vers votre domaine personnel, par exemple mastodon.mondomaine.com
PAPERCLIP_SECRET=un des secrets générés précédemment
SECRET_KEY_BASE=un des secrets générés précédemment
OTP_SECRET=un des secrets générés précédemment

par défaut, dans le docker-compose.yml, l'application et le serveur de streaming écoutent sur les ports 3000 et 4000. Si ces ports sont déjà occupés sur votre serveur, éditez le fichier et remplacez par des ports plus adaptés.

Créez la base de données:

docker-compose run --rm web rails db:migrate

Compilez les fichiers statiques:

docker-compose run --rm web rails assets:precompile

Vous devriez maintenant pouvoir lancer votre instance avec:

docker-compose up -d

Les logs sont visibles via:

docker-compose logs -f

Mise en place du reverse proxy

Si la partie précédente s'est bien passée, votre instance est accessible sur le port 3000 de votre serveur. Cependant, ce n'est pas très pratique pour un usage quotidien, donc nous allons mettre utiliser un reverse proxy, qui recevra les connexions sur les ports 80 et 443 (si vous utilisez https) et redirigera en interne les connexions vers votre instance Mastodon.

Notez qu'il est possible de faire la même chose avec un autre serveur web, comme Apache.

Si vous n'avez pas Nginx d'installé, sous Debian et Ubuntu, vous pouvez faire:

apt-get install nginx

Le dépôt Git de Mastodon fournit ensuite le fichier virtualhost à utiliser pour faire fonctionner le reverse proxy. Il vous suffit d'éditer /etc/nginx/conf.d/mastodon.conf et d'y placer ceci:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    # on redirige les requêtes en clair vers le HTTPS
    listen         80;
    server_name    mastodon.mondomaine.com;
    return         301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;

    # à remplacer
    server_name mastodon.mondomain.com;

    # à remplacer par votre propre certificat ssl
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    keepalive_timeout    70;
    sendfile             on;
    client_max_body_size 0;
    gzip off;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

    location / {
        try_files $uri @proxy;
    }

    location @proxy {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;

        proxy_pass_header Server;
        # si vous avez éditez les ports dans le docker compose, il faut reflêter ce changement ici
        proxy_pass http://localhost:3000;
        proxy_buffering off;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        tcp_nodelay on;
    }

    location /api/v1/streaming {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        # si vous avez éditez les ports dans le docker compose, il faut reflêter ce changement ici
        proxy_pass http://localhost:4000;
        proxy_buffering off;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        tcp_nodelay on;
    }

    error_page 500 501 502 503 504 /500.html;
}

Vérifiez que la configuration est bien valide avec nginx -t, puis redémarrez votre serveur nginx avec service nginx restart. Normalement, votre instance Mastodon devrait être accessible via https://mastodon.mondomaine.com (il faudra bien entendu rajouter les entrées DNS correspondante).

Building a simple Apache log file parser with Python/lifter in 30 lines of code

published on July 12, 2016, 12:59 p.m. by eliotberriot | 0

As you may now, I released lifter publicly a few months ago.

Despite a lot of positive feedback, I've been quite busy over the last months and the project did not evolved much.

One of the reason explaining this situation is that when I initially released lifter, it was only as an ORM for Python iterables. But, thanks to some feedback, I realized that the project as a lot of potential, as a generic ORM that would enable querying any datasource with the same API. I won't go further into details, there is a dedicated issue on GitHub.

Because of that, I took a step back to think of the right architecture that would enable such a use case.

From Python iterables to a log file parser

With lifter, currently, you can run such queries:

from lifter.import models
from lifter.backends.python import IterableStore

class User(models.Model):
    pass

data = [
    {
        'age': 27,
        'is_active': False,
        'email': 'kurt@cobain.music',
    },
    {
        'age': 687,
        'is_active': True,
        'email': 'legolas@deepforest.org',
    },
    {
        'age': 34,
        'is_active': False,
        'email': 'golgoth@lahorde.org',
    }
]

store = IterableStore(data)
manager = store.query(User)
young_users = manager.filter(User.age < 30)

This is quite easy to implement, because everything is Python: the data source, and the API.

Now, what if I want to query my users from a REST API instead, or even a file on my machine ?

I've updated lifter's API to achieve that in the latest lifter release (0.3). As a test of this new implementation, I'll be writing in this guide a simple lifter backend to run queries against an Apache log file.

The backend will have the following capabilities:

  • Convert each log entry to a plain Python model
  • Filtering using log fields (IP, user agent, path requested...)
  • Running more complex queries to extract general data (getting a list of all visitors IP, all referers, total bytes sent over a period...)

The whole thing should fit in 30 lines of code.

Setup

If you want to run the code from this guide, you'll have to download and setup lifter 0.3:

git clone https://github.com/EliotBerriot/lifter.git
cd lifter
git checkout 0.3
python setup.py install

I suggest you run this into a virtual environment for proper isolation (although lifter only requires little dependencies)

The data source

First, we'll have to generate log entries, such as

172.183.134.216 - - [12/Jul/2016:12:22:14 -0700] "GET /wp-content HTTP/1.0" 200 4980 "http://farmer-harris.com/category/index/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; rv:1.9.3.20) Gecko/2013-07-10 02:46:11 Firefox/9.0"

To do so, I'll use a fake log generator. I've edited the project, so you can use it immediatly on lifter, see the example/fake-logs.py file.

So we'll generate 5000 log entries, that will make a sufficient data source to test our backend:

pip install fake-factory numpy  # here we install the faker requirements
python2 example/fake-logs.py -n 5000 > /tmp/apache.log

You can open the /tmp/apache.log file to check everything is okay.

The model

Now we have a real data to play with, let's express it as a lifter model:

from lifter import models

class LogEntry(models.Model):
    ip = models.CharField()
    date = models.DateTimeField()
    method = models.CharField()
    request_path = models.CharField()
    http_version = models.CharField()
    status_code = models.IntegerField()
    response_size = models.IntegerField()
    referrer = models.CharField()
    user_agent = models.CharField()

Nothing fancy here, we declare one LogEntry model with a few fields declarations, corresponding to fields available in the log file.

Some fields are more specifically typed, (response_size with IntegerField and date with DateTimeField), meaning the value for each log entry will be cast to the corresponding python types (integer and datetime).

The adapter

Now we've got our models, we have to write the adapter, that will be responsible for casting each line of our log file as a LogEntry model. First let's get done with the regular expression:

import re

LOG_REGEX = '(?P<ip>[(\d\.)]+) - - \[(?P<date>.*?) -(.*?)\] "(?P<method>\w+) (?P<request_path>.*?) HTTP/(?P<http_version>.*?)" (?P<status_code>\d+) (?P<response_size>\d+) "(?P<referrer>.*?)" "(?P<user_agent>.*?)"'

line = '172.183.134.216 - - [12/Jul/2016:12:22:14 -0700] "GET /wp-content HTTP/1.0" 200 4980 "http://farmer-harris.com/category/index/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; rv:1.9.3.20) Gecko/2013-07-10 02:46:11 Firefox/9.0"'

compiled = re.compile(LOG_REGEX)

match = compiled.match(line)
data = match.groupdict()
print(data)

This will output:

{
    'user_agent': 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; rv:1.9.3.20) Gecko/2013-07-10 02:46:11 Firefox/9.0',
    'date': '12/Jul/2016:12:22:14 -0700',
    'http_version': '1.0',
    'referrer': 'http://farmer-harris.com/category/index/',
    'ip': '172.183.134.216',
    'status_code': '200',
    'method': 'GET',
    'path': '/wp-content',
    'response_size': '4980'
}

The previous output proves that our regex is indeed capturing the data we need.

Now, let's write our adapter:

from lifter.adapters import RegexAdapter

class LogEntryFileAdapter(RegexAdapter):

    regex = LOG_REGEX

    def clean_date(self, data, value, model, field):
        date_format = '%d/%b/%Y:%H:%M:%S'
        return field.to_python(self, value, date_format=date_format)

As you can see, the code is pretty straightforward. We set the regex the adapter will use to parse data, and we only override the clean_date method to provide our custom date format.

Here again, we can test our adapter is working properly:

import datetime

adapter = LogEntryFileAdapter()
instance = adapter.parse(line, LogEntry)

assert instance.ip == '172.183.134.216'
assert instance.date == datetime.datetime(2016, 7, 12, 12, 22, 14)
assert instance.method == 'GET'
assert instance.request_path == '/wp-content'
assert instance.http_version == '1.0'
assert instance.status_code == 200
assert instance.response_size == 4980
assert instance.referrer == 'http://farmer-harris.com/category/index/'
assert instance.user_agent == 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; rv:1.9.3.20) Gecko/2013-07-10 02:46:11 Firefox/9.0'

Wonderful, our log line was successfully converted to a regular lifter model!

Running queries

Until here, everything works properly, but we don't want to create each LogEntry instance by hand. That's where the Store come in:

from lifter.backends import filesystem

store = filesystem.FileStore(path='/tmp/apache.log')
manager = store.query(LogEntry, adapter=LogEntryFileAdapter())

And here we go! With only two lines, we wrapped all our previous code and made our log file queryable.

Maybe you don't believe me, so let's dive into the data:

manager.all().count()
>>> 5000

manager.filter(LogEntry.status_code == 200).count()
>>> 4463

manager.order_by(~LogEntry.response_size).values_list('response_size', 'ip', 'request_path')[:10]
>>> [(5175, '64.219.210.186', '/wp-content'),
     (5163, '157.102.97.30', '/search/tag/list'),
     (5160, '240.199.75.110', '/posts/posts/explore'),
     (5158, '250.236.19.198', '/list'),
     (5156, '154.52.202.165', '/apps/cart.jsp?appID=5056'),
     (5155, '2.132.244.147', '/list'),
     (5149, '20.32.252.68', '/search/tag/list'),
     (5146, '77.223.24.206', '/apps/cart.jsp?appID=9970'),
     (5145, '83.4.117.89', '/explore'),
     (5144, '183.8.119.188', '/list')]

from lifter.aggregates import Avg, Sum, Max, Min
manager.aggregate(Max('response_size'), Min('response_size'), Sum('response_size'), Avg('response_size'))
>>> {'response_size__avg': 4999.165,
     'response_size__max': 5175,
     'response_size__min': 4830,
     'response_size__sum': 24995825}

qs = manager.filter(LogEntry.status_code == 200).exclude(LogEntry.request_path == '/list')

for entry in qs.order_by(LogEntry.date)[:5]:
    print('Hello there, I was generated on {0}'.format(entry.date))

>>> Hello there, I was generated on 2016-07-12 12:19:13
>>> Hello there, I was generated on 2016-07-12 12:22:36
>>> Hello there, I was generated on 2016-07-12 12:28:49
>>> Hello there, I was generated on 2016-07-12 12:29:42
>>> Hello there, I was generated on 2016-07-12 12:32:37

If I remove all checks and prints from the code, we have this:

import datetime

from lifter import models
from lifter.adapters import RegexAdapter
from lifter.backends import filesystem


class LogEntry(models.Model):
    ip = models.CharField()
    date = models.DateTimeField()
    method = models.CharField()
    request_path = models.CharField()
    http_version = models.CharField()
    status_code = models.IntegerField()
    response_size = models.IntegerField()
    referrer = models.CharField()
    user_agent = models.CharField()


class LogEntryFileAdapter(RegexAdapter):

    regex = '(?P<ip>[(\d\.)]+) - - \[(?P<date>.*?) -(.*?)\] "(?P<method>\w+) (?P<request_path>.*?) HTTP/(?P<http_version>.*?)" (?P<status_code>\d+) (?P<response_size>\d+) "(?P<referrer>.*?)" "(?P<user_agent>.*?)"'

    def clean_date(self, data, value, model, field):
        date_format = '%d/%b/%Y:%H:%M:%S'
        return field.to_python(self, value, date_format=date_format)


store = filesystem.FileStore(path='/tmp/apache.log')
manager = store.query(LogEntry, adapter=LogEntryFileAdapter())

Which is exactly 30 lines of code (actually, less if we remove blank lines, but let's not be petty).

Going further

Lifter's documentation is here to help, especially regarding the query language and API.

More backends will be implemented in future releases of lifter (I'd really like a http backend to run queries against a REST API ;).

If you enjoyed this guide, have some questions, or want to spot some mistakes please leave a comment. If you're in to contribute to lifter's development, you're welcome, and I invite you to visit the code repository.

Thank you for reading!

Makake #spammeurdujour

published on April 4, 2016, 4:07 p.m. by eliotberriot | 0

Je reçois régulièrement des emails envoyés par des startups, des étudiants, et des développeurs, qui pensent qu'une adresse mail rendue publique sur GitHub ou Stackoverflow est une autorication implicite de contact pour tout et n'importe quoi. Sondages, études, promotion et email commerciaux plus ou moins ciblés, tous les pretextes sont bons pour inonder leurs cibles d'emails non-sollicités.

Le dernier en date, reçu aujourd'hui, est signé Kevin Muller, co-fondateur et Experts Manager chez Makake. Rien que ça.

Comme j'en ai plein le dos, que je ne souhaite pas masquer mon adresse email car elle est aussi utilisée par des gens qui me contactent, eux, à bon escient, à compter d'aujourd'hui, je publierai systématiquement les emails en question et, le cas échéant, mes réponses (ces spammeurs hypocrites se protègeant souvent eux-mêmes du spam avec des adresses de réponses ne débouchant sur rien).

Donc, le mail du jour:

Bonjour EliotBerriot?,

Je suis Kevin, co-fondateur et Experts Manager chez Makake, le studio numérique d’ingénierie collaborative.

Je vous ai trouvé sur github en cherchant notamment des compétences PHP?. J'ai été séduit par l'ensemble de vos réalisations, alors je me permets de vous contacter afin de vous proposer cette opportunité.

Notre motto chez Makake est la révélation d'experts et de talents auprès de clients qui portent des projets innovants et rémunérés à réaliser. Pour cela, notre web app vous permet de collaborer avec des start-ups tech et grands groupes innovants !

Vous pouvez créer votre profil sur l'app pour que je puisse vous consulter sur les projets en cours : (c'est très rapide)

Si vous ne voulez pas, je comprends tout à fait. Je vous remercie déjà de m'avoir lu jusque là.

N’hésitez pas à me contacter si vous avez des questions, mes coordonnées sont en signature de ce mail. :-)

A bientôt,

Kevin

La réponse :

Bonjour,

Je suis extrèmement déçu de voir que des startups en manque de prospects parsent en permanence les profils GitHub et StackOverflow à leur disposition pour se faire une base d'emailing.

Mon adresse est publique sur GitHub et sur un certain nombre de plateformes car il est parfois nécessaire de me contacter par ce biais afin de collaborer dans le cadre des projets open-source auxquels je participe. Je ne l'ai pas rendue publique pour que n'importe qui me contacte afin de me vendre son nouveau concept "révolutionnaire".

Les spams tels que celui qu vous venez de me faire parvenir nuisent au monde open-source en surchargeant les développeurs de mails non-sollicités. Soyez donc assurés que je ne m'inscrirai jamais sur votre plateforme, et que je vous ferai une mauvaise publicité dès que j'en aurai l'occasion.

Je vais d'ailleurs de ce pas publier notre présent échange sur mon blog.

Eliot

A lire quand tu auras envie de fumer

published on Feb. 3, 2016, 9:01 a.m. by eliotberriot | 1

Cher moi du futur,

Je t'écris ceci car, comme tu le sais, moi du passé a régulièrement essayé d'arrêter de fumer, sans succès.

Bien sûr, les premières semaines se passaient très bien mais, systématiquement, au bout de deux ou trois mois, il finissait par s'y remettre !

Bref, tout ça pour dire, ça fait une semaine que je me passe de tabac, et j'ai bien l'intention que ça dure. Pour éviter le schéma habituel et arrêter définitivement cette fois, j'ai compilé deux listes à ton attention. Promets-moi de les lire avant d'allumer la moindre cigarette, d'accord ?

Tu aimes fumer parce que :

  • La cigarette t'apporte une dose de nicotine (+)
  • Tu t'occupes les mains (++)
  • Tu as du plaisir a jouer avec la fumée, faire des ronds, etc. (++)
  • Tu partage un moment de sociabilité avec d'autres fumeurs (+++)
  • C'est une habitude qui ponctue ta vie, te permets de faire des pauses quand tu le souhaite, etc. (+++)

Mais rassure-toi, tu aimes aussi ne pas fumer ! Quand tu ne fume pas :

  • Tu fais des économies (+)
  • Tout à une meilleure odeur :
    • Tes vêtements (+)
    • Ton appartement (++)
    • Tes mains, tes cheveux, ton haleine... (+++)
  • Tu as un meilleur goût et un meilleur odorat (++)
  • Ton système digestif fonctionne mieux (++)
  • Tes dents sont en meilleur état (++)
  • Tu perds moins de temps (++)
  • Tu respires mieux (+++)
  • Tu dors mieux (+++)
  • Tu ne donnes pas d'argent au lobby du tabac (+++)
  • Tu pollues moins (+++)
  • Tu n'as pas d'addiction à satisfaire (+++)
  • Tu t'accorde une espérance de vie plus longue (+++)
  • Tu es en meilleure santé générale et en meilleure forme (++++)
  • Tu respecte tes proches et leur santé :
    • ceux qui n'aiment pas l'odeur de la cigarette (+++)
    • ceux qui ont des problèmes respiratoires ou risquent d'en développer à force d'être exposés à la fumée (++++)
  • Tu peux aider d'autres fumeurs à arrêter !

Avec tout ça en tête, je te fais confiance pour prendre la bonne décision ;)

Aerials

published on Jan. 15, 2016, 10:35 p.m. by eliotberriot | 0

Soyeuse caresse du vent
Joyeuse envolée qui fait frémir
Les ailes de ton corps beau
Déjà tu frôle les nuages

Nous avons quittés la terre
Nus, à genoux, était-ce une vie ?
Mais nos plumages nous portent
Magie du jeu
Nourriture
L'air mugissant qui nous élève
Efface les larmes

Plus de rage nouée au creux de l'estomac
De cage aux barreaux qui brûlent
De vacarme ou de carnage
Désormais, tu vas, calme
Et mon coeur nage avec toi

The registries pattern

published on Jan. 10, 2016, 5:54 a.m. by eliotberriot | 2

I've been using this pattern for many years in my applications, and I always found strange nobody ever mentioned it. Since it has prove useful to me in many different projects, I think it's a perfect occasion to put some life again in my blog.

Use case

The problem solved by registries could be expressed this way:

Assuming I have a project splitted in many, specific apps, how can these apps communicate together when they need to share transversal data or logic?

I'm working with Python on a daily basis, and more specifically on web applications using the Django framework, so I'll target this ecosystem in my examples.

Existing implementations

If you've used Django, it's almost certain you already manipulated some registries.

Let's take one of the most trivial task in Django, declaring an admin module.

First of all, let's remember what the admin app does: it provides an easy way to generate a web interface to do CRUD (Create/Read/Update/Delete) actions on our project models. Now, how can we tell Django that we want an admin interface for a specific model, without messing with the admin app code?

We're writing a simple blog engine, so our models and admin files would look like this:

# app1/models.py

from django.db import models

class BlogPost(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()

# app1/admin.py

from django.contrib import admin
from .models import BlogPost

class BlogPostAdmin(admin.ModelAdmin):
    pass

admin.site.register(BlogPost, BlogPostAdmin)

Let's explain what the previous lines do:

  1. First, we create a ModelAdmin class. This class will be used to generate an admin interface for our model
  2. Then, via the admin.site.register function, we tell Django: "Hey! I've written an admin module for my model, would you be nice and bind it to my BlogPost model?"

The admin-related code in a Django project goes into an admin.py file inside our app. A project layout could look like:

project/
??? app1
?   ??? __init__.py
?   ??? admin.py
?   ??? models.py
??? app2
    ??? __init__.py
    ??? admin.py
    ??? models.py

Under the hood, when we start our Django server, all admin.py files are imported by the framework, and the admin.site registry is populated with the Model/ModelAdmin bindings we declared. Then, if we visited the admin url in a web browser, we would see all these ModelAdmin.

As you can see, this is a really elegant way to solve the problem: we write our app-specific ModelAdmin in dedicated files, and we inject them inside the admin app in a single line. All admin.py belonging to installed apps are, by convention, discovered and imported automatically by the framework. When the admin interface is displayed, the admin app loops on the registry and uses the bindings to build everything.

A similar pattern is used in various other apps, such as DjangoCMS or django-haystack, even if he registration step is not always explicit. In haystack, for example, there is no register() function, you just need to put your logic inside search_indexes.py files to trigger the registration. Besides that, the whole process is similar.

Leveraging the power of registries

Now, how can we use registries in our projects and, more important, why would we do that?

Imagine you're building a social website with Django. You're website will have a blog, a forum where people would be able to signup and login, and even a basic store to buy goodies and other products.

When building a website, one of the most common brick is the navigation menu. If we write the template for our website menu, we should end up with something like:

<nav class="main-menu">
    <ul>
        <li><a href="{% url 'index' %}">Home</a></li>
        <li><a href="{% url 'blog:index' %}">Blog</a></li>
        <li><a href="{% url 'forum:index' %}">Forum</a></li>
        <li><a href="{% url 'store:index' %}">Store</a></li>
    </ul>
</nav>

We would write this inside a menu.html file in your templates directory, and include this in our base template. It's okay, our menu is quite simple, no big deal.

But then, as time goes, we will add some link to this menu. A login link maybe, and a signup link. And we will want to hide the login and signup links if the user is logged in. And we'll add other apps to our project, that also need their menu links. And we'll want different menus, depending on which section of our website is displayed.

Rapidly, our template will become a horrible mess, full of if clauses, mixing presentation and logic, in short, an unmaintainable, ugly beast.

Let's rephrase our issue: our apps are domain-specific (user, forum, blog, store...), but, somehow, they are all involved in the rendering process of any page of our website, because of the navigation menu. We must find a way to let them plug some logic on the navigation menu template.

Maybe registries can help? ;)

(I wrote a more complete solution for this specific issue, using registries, in a dedicated Django app, named django-navutils, check it out if you're interested!)

Implementing a registry

Here is our project layout:

project/
??? blog
?   ??? __init__.py
?   ??? menu.py
?   ??? models.py
?   ??? views.py
??? forum
?   ??? __init__.py
?   ??? menu.py
?   ??? models.py
?   ??? views.py
??? manage.py
??? project
?   ??? apps.py
?   ??? context_processors.py
?   ??? __init__.py
?   ??? settings.py
?   ??? urls.py
?   ??? utils.py
??? store
?   ??? __init__.py
?   ??? menu.py
?   ??? models.py
?   ??? views.py
??? templates
?   ??? base.html
?   ??? menu.html
??? users
    ??? __init__.py
    ??? menu.py
    ??? models.py
    ??? views.py

The implementation of a registry is quite simple. First you need a method to import arbitrary modules by name. Then you need a method to register data in the registry.

I've already released a real, generic, implementation of a registry, named persisting-theory. The whole thing is available on PyPi and you are free to use it. The following code is heavily inspired from the work I've already done on this package.

So, let's write our registry base class:

from collections import OrderedDict

class Registry(OrderedDict):
    """Our registry use OrderedDict insted of dict as a base class, because I think maintaining the order
    in which data is registered may be useful. Feel free to use dict instead"""

    def autodiscover(self, apps):
        for app in apps:
            try:
                app_package = __import__(app)
                package = '{0}.{1}'.format(app, self.look_into)
                module = __import__(package)
            except ImportError:
                pass

    def register(self, name, data):
        self[name] = data

Done! Of course, it's too naive to be used in production, but this basic prototype will work for our example.

Now, let's create our menu registry:

class MenuRegistry(Registry):
    look_into = 'menu' # the path that our registry will try to import inside each app

menu = MenuRegistry()

You can place this code anywhere, but putting it inside a project/registries.py file seems logic to me. You could also create a menu app for handling all menu-related code.

After that, we write the menu logic:

# project/utils.py

class MenuNode(object):
    def __init__(self, label, url, require_authentication=False):
        self.label = label
        self.url = url
        self.require_authentication = require_authentication

And the template:

<!-- templates/menu.html -->
<nav class="main-menu">
    <ul>
        {% for node in menu_nodes %}
            {% if not node.require_authentication %}
                <li><a href="{% url node.url %}">{{ node.label }}</a></li>
            {% elif node.require_authentication and request.user.is_authenticated %}
                <li><a href="{% url node.url %}">{{ node.label }}</a></li>
            {% endif %}
        {% endfor %}
    </ul>
</nav>

We also add a context processor to pass our menu nodes to the template context:

# project/context_processors.py

from .registries import menu

def menu_nodes(request):
    # remember to add this in your settings.TEMPALTES['context_processors']
    return {'menu_nodes': menu.values()}

And we trigger the autodiscovering process when all apps are ready:

# project/apps.py

from django.apps import AppConfig
from django.conf import settings
from .registries import menu

class ProjectConfig(AppConfig):
    """
    We use Django's built-in app system to trigger autodiscovering when all apps are loaded.
    Remember to add this to `project/__init__.py`:

        default_app_config = 'project.apps.ProjectConfig'
    """
    name = 'project'

    def ready(self):
        # what do we do here? We tell our registry to loop on all the installed apps
        # and to import the menu package, if any"
        menu.autodiscover(settings.INSTALED_APPS)

Here we go. The last thing to do is to register our actual menu nodes:

from project.utils import MenuNode
from project.registries import menu

# project/menu.py
menu.register('index', MenuNode('Home', 'index'))

# blog/menu.py
menu.register('blog', MenuNode('Blog', 'blog:index'))

# forum/menu.py
menu.register('forum_index', MenuNode('Forum', 'forum:index'))

# store/menu.py
menu.register('store_index', MenuNode('Store', 'store:index'))
menu.register('store_basket', MenuNode('Basket', 'store:basket', require_authentication=True))

# users/menu.py
menu.register('login', MenuNode('Login', 'account_login'))
menu.register('signup', MenuNode('Signup', 'account_signup'))
menu.register('settings', MenuNode('Account settings', 'account_settings'))

Voilà! Our registry-based menu is working, assuming corresponding URLs exist in your project. We'll never have to edit our template again when we want to add, edit or remove a menu label. As a bonus, all our menu nodes are stored in the corresponding app directory, as plain Python objects, instead of being all mixed up inside one template file.

We could improve these objects to handle better permission management (e.g. display nodes to anonymous users or staff users only) or display (by adding a css_class argument, or even use custom templates to render nodes), but that's not in the scope of the current post.

Conclusion

It's often overkill to create your own registries if you're working on a small project with only a few apps. But if it grows, I bet you'll miss them.

Also remember that registries really shine when you're writing reusable apps. You're developping a generic, WordPress-like, shortcode app? At some point, you'll need some kind of registry structure to let apps declare and register their own shortcodes.

Because they encourage loose-coupling between apps, and provide an easy to use API, registries are usually a good choice to pick when you face a transversal problematic that involves many different apps, or want to write something that can be easily extended. And because registries are regular data structures subclasses, you can manipulate them as any usual collections and still be able to implement custom behaviour, such as filtering, ordering or registered data validation.

I hope you found this post useful, happy registries!

Comment devenir végétarien ?

published on Oct. 25, 2015, 5:55 p.m. by eliotberriot | 0

Idéalement, j'aurais aimé suivre un parcours comme celui-ci :

  1. Être confronté à des éléments d'informations perturbateurs
  2. Commencer à avoir honte de causer autant de souffrance animale
  3. M'informer plus
  4. Avoir honte des conséquences de l'élevage industriel sur l'environnement, la santé, l'économie, le bien-être humain et animal
  5. Arrêter de consommer de la viande et des produits animaux issus de l'élevage industriel

En réalité, la honte ne suffit pas chez moi à contrebalancer la pression permanente exercée l'omniprésence des produits animaux. Je sais que manger de la viande est nocif pour moi, pour tout le monde, mais quand bien même je parviens à arrêter quelque temps, je finis toujours par céder.

J'envisage donc une autre approche pour cette étape 5. Des baby-steps, en fait :

  1. Continuer à m'informer de manière beaucoup plus régulière pour entretenir la honte
  2. Arrêter d'acheter de la viande pour la maison
  3. Arrêter de consommer de la viande quand je suis hors de chez moi : boulot, repas de famille, déplacements, etc.
  4. Arrêter le poisson
  5. Arrêter les produits animaux type laif/oeuf, etc.

Le 1 ne devrait pas poser de problème, à vrai dire c'est déjà le cas. Le 2 est beaucoup plus compliqué pour moi : cela implique que je cuisine en avance pour le lendemain, que je trouve des endroits où manger végétarien près du boulot et que je complète probablement le tout avec une solution de type Joylent (en gardant à l'esprit que pour le moment, Joylent fait appel à des produits animaux pour sa fabrication).

Le 3 sera une continuité des étapes précédentes, rien d'effrayant.

Le 4 est certainement le plus dûr : autant il est relativement facile de savoir si un produit contient de la viande et de trouver des subtituts non carnivores, autant se passer totalement de lait et surtout d'oeufs compromet des pans entiers de mon alimentation actuelle. C'est le point sur lequel je ferai vraisemblablement des compromis, comme acheter bio, par exemple, bien que cela ne me satisfasse pas complêtement.

J'aimerai arriver à cette étape 2 avant 2016. Si ça ne le fait pas, cela veut peut-être dire que j'ai considéré le problème sous le mauvais angle depuis le début et que je dois opter pour une solution différente. Mais l'arrêt brutal et immédiat que je pratique souvent pour la cigarette par exemple donne des résultats mitigés : je parviens à ne pas fumer du tout pendant plusieurs mois, je craque et me remets à fumer pendant plusieurs mois, puis j'arrête, etc.

Peut-être y-a t'il d'autres solutions que je n'ai pas en tête pour le moment.

Django-navutils, une app django pour gérer les menus et les breadcrumbs

published on June 3, 2015, 9:29 p.m. by eliotberriot | 0

Depuis que j'ai commencé à travailler sur Kii, je me suis posé la question des menus et de la navigation en général :

  • Comment faire pour que différentes applications puisse enregistrer leurs propres éléments dans un menu partagé
  • Comment gérer de façon déclarative les breadcrumbs, ou miettes de pains, qui permettent de savoir où l'on se trouve sur un site en affichant une arborescence de type Accueil > Blog > Bonjour tout le monde
  • Comment afficher des éléments menus de façon dynamique, avec des éléments qui s'affichent ou non en fonction de l'utilisateur (s'il est authentifié, administrateur, s'il possède une permission précise, etc.)
  • Comment générer des éléments menus de façon dynamique, par exemple, un sous-menu contenant la liste des catégories d'un blog

Le système de menu utilisé actuellement dans Kii répond en grande partie à toute ces questions. Cependant, je rencontre ces problématiques dans la plupart des projets Django sur lesquels je travaille, il semblait donc logique de créer une app Django dédiée pour résoudre ce problème.

C'est chose faite depuis environ deux semaines. J'ai en effet publié sur GitHub la bibliothèque django-navutils, ainsi qu'un paquet PyPi associé, basé sur le code initial des menus de Kii, retravaillé en très grande partie.

Django-navutils répond à la plupart des problématiques précédemment cités, et quelques autres, avec une API que j'ai essayé de rendre la plus déclarative possible.

Si vous avez un peu de temps et un projet django sous le coude, allez l'essayer :)

Réupérer son mot de passe Windows 8 sans disque de réinstallation

published on March 28, 2015, 12:36 p.m. by eliotberriot | 0

Mon nouvel ordinateur de travail était livré avec Windows 8. Même si je me suis empressé d'installer Linux (Ubuntu en l'occurence), j'ai tout de même gardé Windows dans un coin au cas où.

Seulement, aujourd'hui, j'ai eu besoin de me connecter dessus et pas moyen de me souvenir de mon mot de passe. La procédure de réinitialisation du mot de passe prévue dans Windows requiert un disque de récupération (CD ou USB), que je n'ai pas fait.

Si vous êtes dans cette situation, ne désespérez pas, il existe un utilitaire nommé chntpw conçu précisément pour ce cas de figure. Dans mon cas, depuis Ubuntu et après avoir monté ma partition Windows, il m'a suffit des commandes suivantes:

sudo apt-get install chntpw

# OS étant le nom de ma partition windows
cd /media/eliotberriot/OS/Windows/System32/config

# on liste les utilisateurs windows
sudo chntpw SAM -l

# on réinitialise le mot de passe utilisateur
sudo chntpw SAM -u eliotberriot

A noter que la dernière commande propose propose plusieurs choix, notamment remettre le mot de passe à zéro ou le remplacer par un nouveau. Le remplacement n'a pas fonctionné pour moi, mais la remise à zéro oui.

Si vous n'avez pas de dual-boot linux accessible pour lancer le programme, vous pouvez le graver sur CD ou clé USB et booter dessus, ça à l'air de marcher aussi.

Quelques liens :

Une verte trêve, nu

published on March 23, 2015, 10:37 p.m. by eliotberriot | 0

Seuls, étonnés, nos sons en notes lues
Égarés, or né tué, ut en rose rage
Fier coeur, crue ocre, if,
Et rêve, gîte, rire, tige verte.

Ennui

published on March 8, 2015, 8:32 p.m. by eliotberriot | 0

Murs blancs troublants
Fenêtre closes
Pan de ciel derrière les rideaux
Soleil dans la rue

Sommeil qu'émousse mon coeur
Reste l'ennui qui rôde
Taraude, leste,
Insidieux
Ainsi dieu
Seigneur qui s'ignore
Maître des lieux
L'être mielleux

Sapant les fondations
Frappant des sons d'aciers
Rouillent le métal et la volition
L'âme étale son abolition
Débrouille l'absurde, émaciée
Fouille, létale, ce qui reste
Elle s'aime assez, s'immisce
Aussi mince qu'un pétale

Débris de moi
Des bruits de pas
Et toujours l'ennui pointe
Jour lent, nuit qui s'invite
Ourlant le ciel derrière la vitre

Tout ça finira bientôt
Et je m'endormirai
Dans un glissement de tes reins

Sauvetage

published on Feb. 23, 2015, 10:02 p.m. by eliotberriot | 0

La main sur le corps
La bouche sur la bouche
Et les yeux qui pleurent

Appuyer encore
Pour sauver ton homme
Glacée par la peur

Rester sur la touche
Et le voir partir
Odeur de sueur

Même s'il n'est pas mort
Tu te sens tout comme
Tu as mal au coeur

L'entend-tu qui dors,
Qui souffle et inspire,
Qui devient plus fort ?

A chaque heure qui passe
Tu n'y crois pas trop
Tu la sens tapie
Dans l'ombre, la menace

Dans la chambre, lasse
Tu ne vois plus rien
Que lui
Et sa poitrine qui se soulève

Tu n'as plus confiance
En l'organique
En la machine humaine
Celle qui vous lache
Au milieu d'un repas
D'une phrase ou d'un orgasme

Tu entends à chaque seconde
Le bruit du dé qu'on jette
Pour voir si c'est terminé

Tu l'avais oublié
Ce roulement feutré, inquiétant
Quel dommage

Vois-le comme un rythme qui ponctue le vide
Une fresque tracée sur un mur blanc
Un animal qui t'accompagne
Une fin pour ceux qui souffrent
La possibilité du nul

Mort sûre

published on Feb. 17, 2015, 1:01 a.m. by eliotberriot | 0

Mort sûre,
L'eau rayée
À l'ouest,
N'attends rien de nous
Veau.

Très triste
Traîtrise
Faux-ami
Fosse amie
Face tueuse
Fumée, râles
Flammes roulent
Lit vide

Je, nous
Jeune ou
Vieux
Jeu nouveau
Noeud.
Joue, veau !

Voeux vides et serments-songes
Sers-m'en cinq, les armes en sang
Voici les arts du serpent sage
Rodant la nuit
Héros dans l'ennui
Érodant l'an nu
L'amitié,
L'âme mitée,
Mu, braise odieuse
Brumeuse audace
Par l'usure

Robe de soirée

published on Feb. 9, 2015, 7:42 p.m. by eliotberriot | 2

Courte et moulante
Presque transparente
Elle m'épouse parfaitement
Je l'ai portée si souvent
Que je l'ai dans la peau

Un film d'ombre
Entre le monde et moi
Elle ne cache rien
Dessous on devine
L'arrondi des hanches
Le haut des cuisses

Je vis avec
Elle ne s'use pas
Je la porte si souvent
Que je ne sens plus ma peau
A quoi ressemblent mes seins
Et mon ventre sous le tissu ?

Je rêve d'elle si souvent
Que je ne vois plus rien d'autre
Il y a autour de moi
Un monde qui m'ennuie
D'autres femmes transparentes

Je ne l'enlève plus
Et je m'use
Je l'ai portée si souvent
Que je n'ai plus de peau

Le danger des symboles

published on Jan. 10, 2015, 4:31 p.m. by eliotberriot | 1

Douze personnes ont été assassinées il y a quelques jours à Charlie Hebdo et je suis inquiet de la tournure que prennent les choses.

Je ne cherche pas à contester aux gens le droit d'être triste : la mort prématurée de douze personnes est un événement terrible, et je comprends que les familles des morts et les lecteurs du journal ressentent le besoin de se retrouver, de se réconforter, d'évoquer des souvenirs, de pleurer, tout simplement.

Seulement, ce qui est en train de se passer va bien au delà. Les morts de Charlie Hebdo sont déjà des symboles, quasiment des martyrs. Des centaines de milliers de gens se focalisent sur ces morts, manifestent, font suivre des chaînes, au point que l'attention générale est obsédée par les événements du 7 janvier. Je pense que cette réaction est disproportionnée et dangereuse.

Hiérarchie des problèmes

Au sein d'une société, tous les problèmes n'ont pas la même importance, et pour que les choses s'améliorent, il est nécessaire de traiter en priorité les problèmes les plus importants, les plus fondamentaux, les plus vastes, pour terminer par les plus spécifiques, anecdotiques.

Par exemple, une réforme économique qui va toucher des millions de gens est plus importante qu'un vol à main armée. Un problème de santé publique internationale compte plus que le salon de l'agriculture. Un risque de catastrophe nucléaire est plus problématique qu'une baisse des bénéfices minime des entreprises du CAC40. Etc.

Pourtant, dans les médias, dans les échanges entre personnes, dans les décisions politiques, les problèmes les plus importants ne sont pas nécessairement ceux qui sont traités en priorité. C'est même bien souvent l'inverse qui se produit, et la situation avec Charlie Hebdo l'illustre très bien.

Au moins 300 000 personnes se sont rassemblées aujourd'hui en hommage aux douze morts de mercredi.

La logique voudrait donc que des millions de français se mobilisent instamment pour sauver un projet de loi sur la qualité de l'air, torpillé par les lobbies, et qui aurait permis d'éviter entre 40 000 et 50 000 morts chaque année. Notez que le papier auquel je fais référence a été publié initialement dans Charlie Hebdo et que personne n'en parle, ce qui prouve que la plupart des gens qui défilent aujourd'hui dans les rues ne lisent de toute façon pas ce journal. Idem, silence quasi-total à ce sujet dans les "grands" médias.

Autre exemple. Saviez-vous que des entreprises françaises, avec la complicité de l'État, vendent des technologies de surveillance de masse à des dictatures ? Ces technologies permettent d'espionner la plus grande partie des communications, à l'échelle d'un pays, d'arrêter et de torturer les opposants politiques, donc de faire taire toute contestation. Personne n'en parle.

Un dernier pour la route, de nouveau tiré de Charlie Hebdo : la propagation de la fièvre Ébola - vous savez, celle qui a déjà 8235 morts au compteur et qui n'est pas partie pour s'arrêter - serait en partie due à la déforestation. Donc à cause de nous et de nos multinationales, en gros.

Des exemples comme ça, il y en a des tas, mais il faut surtout retenir que des milliers de gens au bas mot meurent chaque année, pour des raisons économiques, le tout dans l'indifférence générale. Personne n'en parle donc tout le monde s'en fout. Et pourtant, ces problèmes ont une importance bien plus élevée que ce qui est arrivé à Charlie Hebdo.

Paralysie médiatique

Si personne n'en parle, c'est peut-être à cause de la pression des lobbies, justement, qui n'ont pas franchement intérêt à ce que l'attention publique se concentre sur leur activité. Mais ça passe aussi par ces fameux symboles, ces martyrs, ces causes exemplaires qui se présentent une fois de temps en temps et mobilisent une attention disproportionnée par rapport à leur importance réelle.

Deuil national, chaînes de mail et de SMS, nombreux reportages et articles dans tous les médias, rassemblements populaires... On ne peut pas ne pas penser à Charlie. Même en restant chez soi, sans télévision, sans journaux, un proche va mettre le sujet sur la table.

Des problèmes bien plus importants passent ainsi au second plan, voire disparaissent complètement. C'est également l'occasion rêvée de faire passer en douce des réformes ou des décisions difficiles, pendant que les gens regardent ailleurs.

Quid des rassemblements ?

Enfin, je ne comprends pas ces manifestations. J'ai demandé quel en était l'objectif, on m'a répondu "Pour marquer le coup" ou "Pour la liberté d'expression". En quoi focaliser un pays entier sur un événement est-il positif pour la liberté d'expression ?

S'il s'agit bien d'un attentat terroriste perpétré par des extrémistes religieux, ces manifestations se font contre des personnes fondamentalement irrationnelles, mues par une foi qui nous dépasse - et qui les dépasse aussi, probablement -.

Que faire ?

Ceci dit, je comprends cette réaction, ce choc. Mais peut-être y-a t'il des choses plus utiles à faire ?

Plutôt que de se mobiliser pour des notions vagues, voire sans objectif particulier, pourquoi ne pas rendre hommage à Charlie Hebdo et se mobilisant sur les sujets importants traités par le journal ? Il n'y a qu'à piocher !

Ça peut paraître prétentieux ou moralisateur, mais dans des situations comme celles-ci, j'essaie de garder à l'esprit deux choses :

  • Il faut conserver la tête froide, ne pas réagir dans l'émotion. Cela ne veux pas dire qu'on a pas le droit d'être triste, simplement que la tristesse ne doit pas dicter notre raisonnement.
  • Ce n'est pas parce qu'une chose me touche personnellement qu'elle est importante pour tout le monde.

Puits

published on Jan. 6, 2015, 11:49 p.m. by eliotberriot | 0

En écoutant The Curse

Jourdain sucré
Jour d'un secret
Jardin se crée

Puis s'en va
Puissant fond
Puits sans fond
Puise, enfant
Dans ces limbes
Des syllabes
De réconfort
Denrée qu'on flaire

L'espoir qui brûle
L'histoire qui hurle
Laisse choir

Aspire au vide
Inspire avide
Feu dans ta poitrine
Poison dans l'eau
Air vicié
Terre minée

Des corps rompus
Décor rapé
Accords perdus
Encore happé

Rentre seul
Encre sâle
Âcre sel

Introduction à kii

published on Jan. 2, 2015, 1:34 a.m. by eliotberriot | 0

Dorénavant, et pour une durée indéterminée, je ne publierai plus mes articles sur mon blog mais ici. Si vous souhaitez rester informé des parutions, vous pouvez suivre ce flux : http://next.kii.eliotberriot.com/u/eliotberriot/stream/feed/atom

Aujourd'hui, je voudrais vous parler de kii.

Kii, c'est un projet que j'ai commencé il y a bientôt et demi et qui est né du constat suivant : j'utilise quotidiennement mon lecteur de flux, j'écris régulièrement des articles de blog, et j'ai très souvent besoin de garder une trace des choses que je vois sur le web (mais pas seulement) dans le cadre de ma veille.

Pour gérer ça, avant kii, j'utilisais trois outils distincts, tous accessibles sur le web, à savoir un blog Wordpress, un Shaarli pour les liens, et Kriss Feed pour les flux. Seulement, trois outils, ça veut aussi dire trois base de données, trois comptes utilisateurs, trois mots de passe à retenir, une disparité importante dans les interfaces, des fonctionnalités disponibles sur certains mais pas sur d'autres, trois sites différents à sauvegarder, etc.

Donc au cours de l'été 2014, j'ai commencé à travailler sur kii.

Objectifs

En informatique notamment, il existe le principe KISS (Keep it simple, stupid), qui préconise la simplicité et la sobriété dans la conception.

D'une façon similaire, kii signifie Keep it integrated, car l'objectif initial de kii était de proposer un outil capable de gérer au moins ces trois aspects de façon unifiée, intégrée. Je voulais également :

  • un outil qui soit multi-utilisateurs, pour que des personnes non-techniciennes puisse l'utiliser sans avoir à l'installer sur un serveur
  • quelque chose d'extensible, sur lequel :
    • il soit possible et facile de greffer des fonctionnalités additionnelles
    • il soit possible et facile de remplacer ou de personnaliser des composants déjà inclus
  • en faire un logiciel libre, qui puisse être réutilisé, modifié et redistribué sans contrepartie
  • faciliter autant que possible l'import et l'export depuis/vers d'autres outils en utilisant des formats standards (OPML, format de Favoris Netscape, WXR notamment)
  • un système de commentaire intégré et unifié sur l'ensemble des types de contenus
  • la possibilité d'écrire du contenu dans différentes syntaxes simplifiées, comme Markdown
  • un système de permissions complet, permettant de marquer des object comme privés, réservés à certains utilisateurs/groupes, ou entièrement publics
  • un système de tags, applicable à tous les types de contenus
  • un outil de recherche
  • un système multilingue
  • des flux RSS et Atom regroupant l'ensemble des éléments, mais aussi des flux par type de contenu (pour quelqu'un ne voudrait que les articles du blog par exemple)
  • plein d'autres petites choses, mais l'essentiel est là.

L'échec de la première version

Pour resituer, jusqu'alors, je faisais de la programmation surtout pour m'amuser, mais je disposai d'un niveau acceptable en Python, mon langage de prédilection. Mon expérience du web se limitait à une connaissance plutôt moyenne d'HTML et CSS, et médiocre de PHP.

J'ai commencé à écrire ce projet en PHP, au dessus du framework Symfony, que je ne connaissais que de façon très superficielle, car je l'avais employé dans le cadre d'un projet personnel minuscule.

Très vite, malheureusement je me suis heurté à des limitations techniques bloquantes dans le cadre de ce projet. Même si j'aimais plutôt Symfony et le côté très propre de ce framework, j'ai donc du aller voir ailleurs et, naturellement, je me suis posé la question suivante : peut-on faire du web en Python ?

La réponse est oui. J'ai même été surpris de découvrir que Python était un langage utilisé pour le web sur de très, très gros projets comme YouTube ou Dropbox, pour ne citer qu'eux.

Le principal framework web Python semblait être Django. Sans trop me poser de questions, j'ai donc jeté mon code écrit sous Symfony, et j'ai repris à zéro sur une base Django. Les choses ont été assez lentes au début, dans la mesure où j'ai du prendre le temps de faire connaissance avec le framework.

Néanmoins, j'avais du temps à consacrer à ce projet et les choses ont suivi leur cours. Entre septembre 2013 et mars 2014, j'ai du passer un tiers ou un quart de mon temps à travailler sur cette première version de kii, pour un total de 435 commits. La seule version de kii en production est la mienne, vous pouvez encore en avoir un aperçu pour une durée indéterminée : https://kii.eliotberriot.com/user/eliotberriot/.

Quand je regarde cette version aujourd'hui, j'en vois surtout les défauts, mais rétrospectivement, je trouve que je suis quand même allé assez loin compte tenu de mon manque d'expérience et du fait que j'apprenais simultanément à travailler avec Django : la plupart des fonctionnalités précédemment citée sont implémentées et fonctionnent plus ou moins bien. Il manque le système de commentaires, sur lequel je n'ai pas du tout travaillé, et la partie blog reste vraiment très, très minimaliste.

Malgré ça, le tout fonctionne sans broncher depuis plus d'un an, je l'utilise quotidiennement pour ma veille et pour stocker des liens. Par contre, mon blog est toujours géré à part.

Après mars 2014, j'ai mis fin très brutalement à mon investissement sur ce projet pour me consacrer à mon activité de webdesigner. Je n'ai plus touché au code, par manque de temps, mais aussi à cause d'une prise de conscience progressive. En l'état, kii fonctionne pour moi, est utilisable, mais souffre de plusieurs grave défauts :

  • Le code n'est pas documenté : j'ai écrit très peu de commentaires et pas du tout de documentation, ce qui coupe court à toute possibilité de collaboration avec d'autres développeurs.
  • La qualité du code est médiocre : en dépit de mes efforts, je n'ai pas fait de miracles, et cela se perçoit un peu partout. Dans certains ce ne sont que des défauts anodins, mais dans d'autres, ce sont des pans essentiels qui demanderaient une réécriture, pour des raisons de de maintenabilité, de sécurité, de performance, de qualité d'API...
  • Le code n'est pas testé : enfin, il est testé à la main, mais je n'ai pas écrit un seul test unitaire pour kii. Vu le nombre de lignes de code, et le manque de documentation, il m'est arrivé de plus en plus souvent d'introduire de nouveaux bugs en essayant de résoudre les anciens, ce que permettent d'éviter les tests unitaires. Sur la fin, cela a contribué à casser ma motivation, puisque chaque minuscule modification entraînait systématiquement des conséquences plus ou mois grave, que je passais un temps fou à résoudre au lieu d'avancer.
  • Le code est déjà vieux : j'ai codé exclusivement pour Python 2, alors que la version 3 est en train de devenir la référence de facto.
  • Le code n'est pas suffisamment extensible : coder une application pour la version actuelle de kii est faisable mais requiert une quantité impressionnante de code finalement inutile, ce qui j'aurais pu éviter si j'avais fait les choses différemment.
  • Le code n'est pas intégrable au sein d'autres projets Django.
  • Le projet est trop spécifique : mon cas d'utilisation (blog + partage de lien + lecture de flux) n'est pas forcément si répandu. Je pense aujourd'hui qu'intégrer ces trois aspects directement dans le cœur de l'application plutôt que de les développer séparément était une erreur.

Bien sûr, il y aurait encore énormément de choses à dire, mais vous avez compris l'idée.

L'essai d'une deuxième version

Vu le titre précédent, vous vous doutez que je ne me suis pas arrêté là. Ça m'aura pris six mois, le temps d'une grosse coupure et de travailler sur d'autres projets, de découvrir de nouveaux outils et de nouvelles pratiques mais je m'y suis remis.

Et j'ai tout repris de zéro. Je crois que je n'ai pas copié une seule ligne de code depuis l'ancienne version, c'est vous dire. Pour éviter de tomber dans les écueils cités précédemment, j'ai fait un certain nombre de choix :

  • Je m'oblige à écrire les tests unitaires avant les fonctionnalités, dans une approche de Test Driven Development (TDD).
  • Je découple au maximum les fonctionnalités, et ne laisse dans le cœur que ce qui est vraiment fondamental. Comme ce sont des applications distinctes qui fournissent les fonctionnalités finales (comme l'aspect blog), kii est plus une bibliothèque sur laquelle s'appuyer qu'une application web prête à l'emploi. Cela rend kii potentiellement utilisable sur des cas d'utilisation différent de ce que j'avais imaginé au départ. Par ailleurs, le fait de travailler séparément sur les applications finales m'oblige à utiliser ma propre API, donc à l'écrire proprement.
  • Je code simultanément pour Python 2 et 3.
  • J'essaie de rendre kii le moins intrusif possible pour qu'il puisse s'intégrer sans douleur au sein de projets Django plus vastes.
  • Je me force à documenter mon code et à rendre le fonctionnement de kii plus facile à appréhender.

Cette deuxième mouture n'est pas encore stable, mais le cœur intègre déjà une grosse partie des fonctionnalités de ma roadmap intérieure, le tout avec un code beaucoup plus propre, consultable ici : https://code.eliotberriot.com/kii/kii. Je publie également kii sur PyPi, ce qui facilite grandement le processus de déploiement et de mise à jour d'une instance.

Je travaille en parallèle sur l'application de blog et je pense avoir atteint le point où le développement du cœur et du blog est suffisamment avancé pour les rendre utilisable. Pour vérifier que je ne me trompe pas, il faut donc que je devienne cobaye (principe du dogfooding), et c'est ce que je fais avec ce premier article.

La prochaine étape sera de rédiger une jolie documentation qui donne envie à la fois aux utilisateurs finaux et aux développeurs. J'ai déjà commencé, mais ça demeure embryonnaire.

Je me fixe comme objectif de commencer à parler du projet, par exemple sur des forums, dans les trois mois qui viennent, afin de commencer à obtenir du feedback. En attendant, si vous voulez me donner votre avis, les commentaires sont ouverts ;)

Quoi qu'il en soit, merci d'avoir pris le temps de me lire. Je vous souhaite une très belle année 2015 !