27 Nov 2015, 00:10

Installer Python 3 5 1rc1

La version 3.5.1rc1 de Python est sortie cette semaine, incluant de nombreuses corrections.

Voici comment l’installer si vous souhaitez la tester. Sur mon laptop j’ai un répertoire dans lequel je place les sources des différentes versions de Python (~/dev/python/src), et un autre dans lequel je compile les binaires (~/dev/python/bin).

On commence par télécharger les sources :

ncrocfer@home:~$ cd ~/dev/python/src
ncrocfer@home:~/dev/python/src$ wget https://www.python.org/ftp/python/3.5.1/Python-3.5.1rc1.tgz
ncrocfer@home:~/dev/python/src$ tar xzvf Python-3.5.1rc1.tgz
ncrocfer@home:~/dev/python/src$ cd Python-3.5.1rc1/

On compile ensuite les sources, en choisissant le répertoire de destination souhaité :

ncrocfer@home:~/dev/python/src/Python-3.5.1rc1$ ./configure --prefix="/home/ncrocfer/dev/python/bin/3.5.1rc1/"
ncrocfer@home:~/dev/python/src/Python-3.5.1rc1$ make
ncrocfer@home:~/dev/python/src/Python-3.5.1rc1$ make install

Vous pouvez désormais tester Python 3.5.1rc1 :

ncrocfer@home:~$ cd ~/dev/python/bin/3.5.1rc1/bin/
ncrocfer@home:~/dev/python/bin/3.5.1rc1/bin$ ./python3
Python 3.5.1rc1 (default, Nov 27 2015, 00:32:21)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

A noter que cette méthode fonctionne évidemment pour les autres versions de Python. Utile si vous devez tester votre code sur une version spécifique de Python non packagée dans votre système de paquet.

24 Nov 2015, 23:18

netstat sur Debian 8 Jessie

Lorsqu’on utilise docker pour développer, on ne sait pas toujours sur quel OS on va tomber. Tout dépend du choix du créateur de l’image : on tombera parfois sur du Debian, parfois sur du Ubuntu, parfois encore sur du CentOS.

En revanche je me suis rendu compte que la plupart (la totalité ?) des images officielles proposées par Docker étaient quant à elles basées sur Debian. Même si parfois elles diffèrent de version (exemple avec l’image Redis sous Wheezy et l’image RabbitMQ sous Jessie), on reste quand même sur du Debian. Et l’image officielle Python ne fait pas exception à cette règle.

Du coup pas de netstat de base pour tester si votre gunicorn écoute sur le bon port ou s’il est accessible sur localhost ou sur 0.0.0.0 (open bar).

Alors si comme moi vous avez bêtement tenté de rechercher le paquet netstat et que vous êtes tombé sur ça :

ncrocfer@home:~$ docker run -it python:3.4 /bin/bash
root@caa036eb3980:/# apt-get update
...
...
root@caa036eb3980:/# apt-cache search netstat
libparse-netstat-perl - module to parse the output of the "netstat" command
netstat-nat - tool that display NAT connections
root@caa036eb3980:/#

Eh bien sachez que l’outil se situe dans le paquet net-tools :

root@caa036eb3980:/# apt-get install net-tools

Vous pouvez maintenant lancer vos services et vérifiez leurs connexions :

root@caa036eb3980:/# pip install flask
root@caa036eb3980:/# cat > app.py <<- EOM
> from flask import Flask
> app = Flask(__name__)
>
> @app.route("/")
> def hello():
>     return "Hello World!"
>
> if __name__ == "__main__":
>     app.run()
> EOM
root@caa036eb3980:/# python app.py &
[1] 104
root@caa036eb3980:/# netstat -plnt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      -
root@caa036eb3980:/#

27 Sep 2015, 18:16

Formater du JSON dans le terminal

Il m’arrive de devoir lire un fichier JSON dans le terminal. Dans bien des cas malheureusement ce fichier n’est pas formaté et la lecture est quasiment impossible.

Par exemple avec ce fichier customers.json :

$ more customers.json
    [ { "_id": "56081479e15e4ab842a896e8", "index": 0, "guid": "5a6b6434-e2c0-4c03-8103-a7009132a3c6", "isActive": false, "balance": "$3,324.04", "picture": "http://placehold.it/32x32", "age": 30, "eyeColor": "brown", "name": "Joan Maldonado", "gender": "female", "company": "PLASMOS", "email": "joanmaldonado@plasmos.com", "phone": "+1 (882) 446-3244", "address": "904 Will Place, Vincent, New Hampshire, 791", "about": "Enim velit reprehenderit aliquip nulla sint. Commodo et magna pariatur Lorem. Ut esse ut aliquip nulla et labore aute veniam qui ex et. Do proident elit occaecat id enim id ad fugiat nostrud. Amet non minim cupidatat sint voluptate ullamco. Consequat commodo sunt elit mollit consequat labore nisi excepteur sint ex consequat.\r\n", "registered": "2014-07-22T10:21:02 -02:00", "latitude": 20.049304, "longitude": -116.525111, "tags": [ "in", "non", "esse", "Lorem", "dolor", "id", "officia" ], "friends": [ { "id": 0, "name": "Gardner Reese" }, { "id": 1, "name": "Iva Flores" }, { "id": 2, "name": "Walters Young" } ], "greeting": "Hello, Joan Maldonado! You have 3 unread messages.", "favoriteFruit": "apple" }, { "_id": "56081479db98561a62a7679d", "index": 1, "guid": "6109f36b-c17b-4a7d-9c3d-82b5afed5b08", "isActive": false, "balance": "$2,768.70", "picture": "http://placehold.it/32x32", "age": 26, "eyeColor": "green", "name": "Tisha Patel", "gender": "female", "company": "KIOSK", "email": "tishapatel@kiosk.com", "phone": "+1 (860) 531-3279", "address": "500 Caton Place, Rockingham, Kansas, 7867", "about": "Magna do ullamco commodo Lorem ullamco eu sit enim dolor. Enim est eiusmod irure commodo ad duis veniam officia dolor incididunt veniam in proident. Magna eiusmod cillum sit nisi ullamco nostrud velit laboris sint enim ad voluptate nulla quis. Voluptate consectetur excepteur eiusmod sunt. Labore officia ea laborum quis tempor magna laboris nostrud aute adipisicing non.\r\n", "registered": "2015-08-23T12:53:27 -02:00", "latitude": -40.008583, "longitude": 94.809866, "tags": [ "dolore", "et", "laborum", "occaecat", "aute", "laboris", "officia" ], "friends": [ { "id": 0, "name": "Phyllis Mccullough" }, { "id": 1, "name": "Mercedes Bullock" }, { "id": 2, "name": "Virginia Witt" } ], "greeting": "Hello, Tisha Patel! You have 8 unread messages.", "favoriteFruit": "apple" }, { "_id": "5608147988cc9bbeebd14516", "index": 2, "guid": "f0bb0639-5d2a-47e1-883d-21fb53fb3d4c", "isActive": true, "balance": "$2,235.78", "picture": "http://placehold.it/32x32", "age": 23, "eyeColor": "brown", "name": "Elvia Gaines", "gender": "female", "company": "GYNK", "email": "elviagaines@gynk.com", "phone": "+1 (991) 513-2225", "address": "612 Canton Court, Grapeview, Michigan, 1411", "about": "Ipsum incididunt ad occaecat occaecat ipsum adipisicing sunt veniam aliquip commodo. Ad mollit elit anim nulla dolor aute elit qui tempor cupidatat anim nulla incididunt. Lorem ex non et aute laborum. Et esse qui consequat quis labore. Irure commodo incididunt Lorem magna ullamco. Reprehenderit consequat tempor cupidatat eiusmod magna in laboris adipisicing.\r\n", "registered": "2015-08-13T05:05:02 -02:00", "latitude": -72.222724, "longitude": 56.951741, "tags": [ "ullamco", "cillum", "excepteur", "proident", "do", "do", "voluptate" ], "friends": [ { "id": 0, "name": "Jarvis Lambert" }, { "id": 1, "name": "Carla Ferguson" }, { "id": 2, "name": "Cline Combs" } ], "greeting": "Hello, Elvia Gaines! You have 7 unread messages.", "favoriteFruit": "strawberry" }, { "_id": "56081479a320a31282066ffb", "index": 3, "guid": "11d34616-a325-4326-a4dd-260f26950ca8", "isActive": true, "balance": "$2,083.11", "picture": "http://placehold.it/32x32", "age": 24, "eyeColor": "blue", "name": "Sallie Barr", "gender": "female", "company": "TRANSLINK", "email": "salliebarr@translink.com", "phone": "+1 (911) 454-2623", "address": "714 Doughty Street, Grahamtown, North Dakota, 4512", "about": "Non incididunt do anim culpa adipisicing nostrud ullamco ea tempor. Enim duis quis pariatur excepteur ut voluptate et amet ut sint voluptate. Consectetur id irure mollit adipisicing veniam eiusmod veniam occaecat. Dolor consequat nulla dolor id velit eu irure. Cillum velit ad sunt fugiat officia cupidatat fugiat et consectetur amet tempor. Duis fugiat ipsum tempor ipsum fugiat nisi pariatur dolor cupidatat adipisicing ea nostrud velit. Eu nulla laborum fugiat ipsum occaecat non est quis do voluptate minim et officia.\r\n", "registered": "2014-08-10T03:51:55 -02:00", "latitude": -74.14838, "longitude": 24.023487, "tags": [ "elit", "proident", "aliquip", "culpa", "dolor", "cupidatat", "do" ], "friends": [ { "id": 0, "name": "Aida Scott" }, { "id": 1, "name": "Cash Gillespie" }, { "id": 2, "name": "Bryan Hoover" } ], "greeting": "Hello, Sallie Barr! You have 4 unread messages.", "favoriteFruit": "apple" }, { "_id": "56081479a03f0f3f3f359e7b", "index": 4, "guid": "d9bfb305-7c94-43ec-974a-12addf238468", "isActive": false, "balance": "$3,170.81", "picture": "http://placehold.it/32x32", "age": 39, "eyeColor": "brown", "name": "Frances Martinez", "gender": "female", "company": "CODACT", "email": "francesmartinez@codact.com", "phone": "+1 (887) 504-2517", "address": "609 Balfour Place, Emory, Colorado, 6617", "about": "Incididunt consequat deserunt reprehenderit eu laborum aliquip duis non fugiat mollit fugiat. Velit do eiusmod fugiat consequat aliquip fugiat anim mollit aliquip cupidatat cupidatat Lorem reprehenderit est. Dolore voluptate culpa labore minim. Commodo sunt commodo sint in exercitation aliquip aute aute sunt deserunt ea occaecat est irure. Minim aliqua proident est excepteur. Proident minim deserunt dolore adipisicing aliquip ipsum enim est consectetur amet fugiat excepteur ex. Tempor non officia exercitation ipsum ipsum est reprehenderit reprehenderit pariatur nulla fugiat reprehenderit consequat mollit.\r\n", "registered": "2014-07-18T08:39:15 -02:00", "latitude": -11.888415, "longitude": 150.058245, "tags": [ "enim", "culpa", "deserunt", "est", "elit", "veniam", "voluptate" ], "friends": [ { "id": 0, "name": "Mooney Kelly" }, { "id": 1, "name": "Maddox Mills" }, { "id": 2, "name": "Olsen Perkins" } ], "greeting": "Hello, Frances Martinez! You have 1 unread messages.", "favoriteFruit": "strawberry" }, { "_id": "56081479d6f7624b753fbd24", "index": 5, "guid": "f6f40203-43b5-46b4-90ef-4e03ac0410d9", "isActive": true, "balance": "$2,787.96", "picture": "http://placehold.it/32x32", "age": 34, "eyeColor": "green", "name": "Houston Kerr", "gender": "male", "company": "INCUBUS", "email": "houstonkerr@incubus.com", "phone": "+1 (912) 474-3368", "address": "802 Preston Court, Cataract, Kentucky, 5816", "about": "Eu amet nulla veniam nostrud magna id. Veniam elit qui exercitation esse esse excepteur reprehenderit ullamco et. Dolore officia Lorem pariatur tempor dolore Lorem quis enim ad aliqua. Sit sunt culpa aute consequat fugiat. Magna minim duis ea cillum quis duis proident tempor deserunt velit cupidatat nulla. Culpa eiusmod consectetur eiusmod labore.\r\n", "registered": "2014-10-12T09:08:48 -02:00", "latitude": 33.730743, "longitude": -17.213511, "tags": [ "sunt", "dolor", "sit", "sit", "eu", "ad", "ex" ], "friends": [ { "id": 0, "name": "Marguerite Skinner" }, { "id": 1, "name": "Webster Sykes" }, { "id": 2, "name": "Wendy Campos" } ], "greeting": "Hello, Houston Kerr! You have 5 unread messages.", "favoriteFruit": "strawberry" } ]

Pour pallier à ce problème une solution est d’utiliser le module JSON de Python :

$ python -m json.tool customers.json
[
    {
        "_id": "56081479e15e4ab842a896e8",
        "about": "Enim velit reprehenderit aliquip nulla sint. Commodo et magna pariatur Lorem. Ut esse ut aliquip nulla et labore aute veniam qui ex et. Do proident el
it occaecat id enim id ad fugiat nostrud. Amet non minim cupidatat sint voluptate ullamco. Consequat commodo sunt elit mollit consequat labore nisi excepteur sint ex co
nsequat.\r\n",
        "address": "904 Will Place, Vincent, New Hampshire, 791",
        "age": 30,
        "balance": "$3,324.04",
        "company": "PLASMOS",
        "email": "joanmaldonado@plasmos.com",
        "eyeColor": "brown",
        "favoriteFruit": "apple",
        ...
    }
]

En revanche la coloration syntaxique n’est pas prise en compte (bon ici le résultat est coloré, mais ce n’est pas le cas dans le terminal). Pour cela on peut utiliser jq, qui est un parser complet de JSON intégrant une tonne d’options. Nous allons simplement l’utiliser pour formater et colorer notre fichier :

$ jq "." customers.json
[
  {
    "favoriteFruit": "apple",
    "greeting": "Hello, Joan Maldonado! You have 3 unread messages.",
    "friends": [
      {
        "name": "Gardner Reese",
        "id": 0
      },
      {
        "name": "Iva Flores",
        "id": 1
      },
      {
        "name": "Walters Young",
        "id": 2
      }
    ],
    ...
  }
]

Le “.” est l’expression de base de jq qui permet de renvoyer le résultat sans le modifier.

Bien sûr je ne montre ici que l’utilisation basique de l’outil. Comme je le disais avant il existe énormément d’options et je vous invite à lire la doc pour en savoir plus.

14 Sep 2015, 23:19

Séparer un entier tous les n chiffres

Sous Python, le découpage d’une chaîne de caractères en plusieurs parties s’effectue grâce à la méthode split(). Mais parfois cette dernière ne suffit pas : le découpage d’un entier par bloc de 3 caractères par exemple.

Dans ce cas il faudra en effet partir de la fin de l’entier : 2000000 deviendra 2 000 000, 1234 deviendra 1 234, etc.

Ce type d’affichage est régulièrement utilisé sur certains sites pour les statistiques, comme ici chez StackOverflow :

Voici la méthode que j’utilise pour cela :

def split_int(number, separator=' ', count=3):
    return separator.join(
        [str(number)[::-1][i:i+count] for i in range(0, len(str(number)), count)]
    )[::-1]

On peut l’utiliser pour reproduire l’affichage de StackOverflow :

>>> split_int(10144157, ',')
10,144,157

Edit

J’ai reçu un tweet de @SamEtMax proposant une solution incluse dans la stdlib. Elle fonctionne grâce à la lib locale et sa méthode format, qui dispose de l’argument grouping.

L’avantage principale de cette méthode est qu’elle se base sur la localisation de l’utilisateur : en France nous séparons les milliers par un espace (1 024), les Anglais utilisent la virgule (1,024) par exemple.

La lib locale convertira l’entier directement sous le bon format :

>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'en_US.utf8')
'en_US.utf8'
>>> print(locale.format("%d", 1024, grouping=True))
1,024
>>> locale.setlocale(locale.LC_ALL, 'fr_FR.utf8')
'fr_FR.utf8'
>>> print(locale.format("%d", 1024, grouping=True))
1 024

Je ne connaissais pas et j’avoue que c’est beaucoup plus simple que ma fonction custom, je passerai par cette solution désormais :)

27 Aug 2015, 18:05

Création d'API Avec Scrapy et Django

La majorité des API se font désormais au format REST. Sous Django, de telles API peuvent facilement se créer grâce au toolkit Django REST framework.

Nous allons voir dans cet article comment créer une API REST sous Django. Pour que l’exemple soit ludique, nous créerons une API fournissant la liste complète des bières belge ! Pour cela nous utiliserons Scrapy afin de parser la page Wikipédia correspondante, puis nous utiliserons les modèles Django pour insérer les données en base.

Setup

On commence par créer un nouvel environnement virtuel qui contiendra l’ensemble des packages utiles à notre projet :

$ mkvirtualenv beer-rest
$ pip install Scrapy scrapy-djangoitem Django djangorestframework

Dans l’ordre :

  • Scrapy va nous servir à parser la page Wikipédia et retourner les informations souhaitées,
  • scrapy-djangoitem nous permettra d’utiliser les modèles Django dans notre projet Scrapy,
  • Django sera notre framework web,
  • djangorestframework va nous permettre de créer facilement notre API REST.

Nous pouvons maintenant créer notre projet Django alcohol, ainsi que l’application beer :

$ django-admin startproject alcohol
$ cd alcohol
$ ./manage.py startapp beer

Création du modèle

La page Wikipédia suivante propose la liste complète des bières belges, par ordre alphabétique.

Nous allons enregistrer en base les informations données, à savoir :

  • le nom de la bière
  • le type
  • la teneur en alcool
  • la brasserie

Voici le modèle correspondant :

# ./alcohol/beer/models.py

from django.db import models

class Beer(models.Model):
    nom = models.CharField(max_length=80)
    type = models.CharField(max_length=30)
    degre = models.CharField(max_length=6, null=True)
    brasserie = models.CharField(max_length=80)

    def __str__(self):
        return self.nom

Certaines teneurs en alcool ne sont pas mentionnées, nous autorisons donc la valeur null.

On active cette application dans le fichier settings.py de notre projet Django :

# ./alcohol/alcohol/settings.py

INSTALLED_APPS = (
    ...
    'beer'
)

On synchronise ensuite la base de données :

$ ./manage.py makemigrations
$ ./manage.py migrate

Récupération des bières avec Scrapy

Nous allons utiliser Scrapy pour extraire les données et les insérer en base.

Toujours dans le dossier alcohol, nous créons un dossier scrapy qui contiendra nos crawlers (nous n’en aurons qu’un dans notre cas), puis nous initialisons un nouveau projet Scrapy :

$ mkdir scrapy
$ cd scrapy
$ scrapy startproject wikibeer
$ cd wikibeer

Nous devrions normalement créer une sous-classe de scrapy.Item, contenant la liste des données souhaitées (nom, type, degre, brasserie). Néanmoins nous voulons que les données récupérées par Scrapy soient directement accessible via Django. Pour cela nous allons utiliser le package scrapy-djangoitem qui se chargera lui-même d’insérer les éléments en base.

Voici donc à quoi ressemble notre classe item :

# -*- coding: utf-8 -*-
# ./alcohol/scrapy/wikibeer/wikibeer/items.py

from scrapy_djangoitem import DjangoItem
from beer.models import Beer

class WikibeerItem(DjangoItem):
    django_model = Beer

L’accès au modèle Beer de notre projet Django est rendu possible en ajoutant les lignes suitantes au fichier settings.py de votre projet Scrapy :

# -*- coding: utf-8 -*-
# ./alcohol/scrapy/wikibeer/wikibeer/settings.py

import sys
sys.path.append('/opt/projects/alcohol')

import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'alcohol.settings'

...

Pensez à modifier le chemin selon votre configuration.

Il ne reste plus qu’à créer notre spider qui se chargera de retourner les données contenues dans la page :

# -*- coding: utf-8 -*-
# ./alcohol/scrapy/wikibeer/wikibeer/spiders/wikibeer_spider.py

import scrapy
from wikibeer.items import WikibeerItem


class WikibeerSpider(scrapy.Spider):
    name = "wikibeer"
    allowed_domains = ["fr.wikipedia.org"]
    start_urls = [
        "https://fr.wikipedia.org/wiki/Liste_des_bi%C3%A8res_belges",
    ]

    def parse(self, response):
        for sel in response.xpath('//table[contains(@class, "wikitable")]//tr'):

            # On vérifie qu'il ne s'agit pas du header du tableau
            nom = sel.xpath('td[1]//text()').extract()
            if not nom:
                continue

            item = WikibeerItem()
            item['nom'] = "".join(nom)
            item['type'] = "".join(sel.xpath('td[2]//text()').extract())
            item['degre'] = "".join(sel.xpath('td[3]//text()').extract())
            item['brasserie'] = "".join(sel.xpath('td[4]//text()').extract())

            item.save()

Je ne m’attarde pas sur ce code, n’hésitez pas à jeter un oeil à la documentation de Scrapy si vous voulez plus d’informations. Scrapy est un crawler web extrèmement puissant, et le spider précédent n’exploite qu’une infime partie de ses possibilités.

Notez tout de même l’utilisation de la méthode save() sur l’item (plutôt que de le renvoyer via un yield) qui permet d’enregistrer l’objet en base.

Le spider peut enfin être lancé grâce à la commande suivante :

$ scrapy crawl wikibeer
2015-08-27 18:23:50 [scrapy] INFO: Scrapy 1.0.1 started (bot: wikibeer)
2015-08-27 18:23:50 [scrapy] INFO: Optional features available: ssl, http11
2015-08-27 18:23:50 [scrapy] INFO: Overridden settings: {'NEWSPIDER_MODULE': 'wikibeer.spiders', 'SPIDER_MODULES': ['wikibeer.spiders'], 'BOT_NAME': 'wikibeer'}
2015-08-27 18:23:50 [scrapy] INFO: Enabled extensions: CloseSpider, TelnetConsole, LogStats, CoreStats, SpiderState
2015-08-27 18:23:50 [scrapy] INFO: Enabled downloader middlewares: HttpAuthMiddleware, DownloadTimeoutMiddleware, UserAgentMiddleware, RetryMiddleware, DefaultHeadersMiddleware, MetaRefreshMiddleware, HttpCompressionMiddleware, RedirectMiddleware, CookiesMiddleware, ChunkedTransferMiddleware, DownloaderStats
2015-08-27 18:23:50 [scrapy] INFO: Enabled spider middlewares: HttpErrorMiddleware, OffsiteMiddleware, RefererMiddleware, UrlLengthMiddleware, DepthMiddleware
2015-08-27 18:23:50 [scrapy] INFO: Enabled item pipelines:
2015-08-27 18:23:50 [scrapy] INFO: Spider opened
2015-08-27 18:23:50 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2015-08-27 18:23:51 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2015-08-27 18:23:51 [scrapy] DEBUG: Crawled (200) <GET https://fr.wikipedia.org/wiki/Liste_des_bi%C3%A8res_belges> (referer: None)
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'BEGIN' - PARAMS = (); args=None
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.001) QUERY = u'INSERT INTO "beer_beer" ("nom", "type", "degre", "brasserie") VALUES (%s, %s, %s, %s)' - PARAMS = (u'3 Scht\xe9ng', u'Fermentation haute', u'6\xa0%', u"Brasserie Grain d'Orge"); args=[u'3 Scht\xe9ng', u'Fermentation haute', u'6\xa0%', u"Brasserie Grain d'Orge"]
2015-08-27 18:23:52 [scrapy] INFO: Received SIGINT, shutting down gracefully. Send again to force
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'BEGIN' - PARAMS = (); args=None
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'INSERT INTO "beer_beer" ("nom", "type", "degre", "brasserie") VALUES (%s, %s, %s, %s)' - PARAMS = (u'IV Saison', u'Saison', u'6,5\xa0%', u'Brasserie de Jandrain-Jandrenouille'); args=[u'IV Saison', u'Saison', u'6,5\xa0%', u'Brasserie de Jandrain-Jandrenouille']
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'BEGIN' - PARAMS = (); args=None
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'INSERT INTO "beer_beer" ("nom", "type", "degre", "brasserie") VALUES (%s, %s, %s, %s)' - PARAMS = (u'V Cense', u'Fermentation haute, Sp\xe9ciale', u'7,5\xa0%', u'Brasserie de Jandrain-Jandrenouille'); args=[u'V Cense', u'Fermentation haute, Sp\xe9ciale', u'7,5\xa0%', u'Brasserie de Jandrain-Jandrenouille']
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'BEGIN' - PARAMS = (); args=None
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'INSERT INTO "beer_beer" ("nom", "type", "degre", "brasserie") VALUES (%s, %s, %s, %s)' - PARAMS = (u'VI Wheat', u'Fermentation haute, Blanche', u'6\xa0%', u'Brasserie de Jandrain-Jandrenouille'); args=[u'VI Wheat', u'Fermentation haute, Blanche', u'6\xa0%', u'Brasserie de Jandrain-Jandrenouille']
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'BEGIN' - PARAMS = (); args=None
2015-08-27 18:23:52 [django.db.backends] DEBUG: (0.000) QUERY = u'INSERT INTO "beer_beer" ("nom", "type", "degre", "brasserie") VALUES (%s, %s, %s, %s)' - PARAMS = (u'26.2 M', u'Blonde', u'4,8\xa0%', u"Brasserie L'\xc9chapp\xe9e Belle"); args=[u'26.2 M', u'Blonde', u'4,8\xa0%', u"Brasserie L'\xc9chapp\xe9e Belle"]
...

Les données sont bien enregistrées en base de données, le tout en passant par notre modèle Django :

Mise en place de Django Rest Framework

Nous avons maintenant la liste entière des bières dans notre base de données. Nous pouvons créer un serializer basé sur notre modèle Beer :

# ./alcohol/beer/serializers.py

from beer.models import Beer
from rest_framework import serializers

class BeerSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Beer
        fields = ('nom', 'type', 'degre', 'brasserie')

Nous créons ensuite une vue qui sera une sous-classe de rest_framework.viewsets.ModelViewSet dans laquelle nous renseignons la requête utilisée (toutes les bières) et le serializer créé précédemment :

# ./alcohol/beer/views.py

from rest_framework import viewsets
from beer.models import Beer
from beer.serializers import BeerSerializer

class BeerViewSet(viewsets.ModelViewSet):
    queryset = Beer.objects.all()
    serializer_class = BeerSerializer

Les urls de notre projet doivent être renseignées dans le fichier alcohol/urls.py (dans notre cas l’API sera accessible via http://localhost:8000/api/beers/ :

# ./alcohol/alcohol/urls.py

from django.contrib import admin
from django.conf.urls import url, include
from rest_framework import routers
from beer.views import BeerViewSet

router = routers.DefaultRouter()
router.register(r'api/beers', BeerViewSet)

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^', include(router.urls))
]

Il ne reste plus qu’à activer l’application Django Rest Framework dans le fichier settings.py de notre projet Django :

# ./alcohol/alcohol/settings.py

INSTALLED_APPS = (
        ...
        'rest_framework',
        'beer'
)

Test de l’API

Et voilà ! Nous avons rapratrier un jeu de données via Scrapy, nous avons mis en place notre API via Django Rest Framework, nous pouvons désormais la tester.

On lance tout d’abord le serveur :

$ ./manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
August 27, 2015 - 18:28:08
Django version 1.8.4, using settings 'alcohol.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Puis nous utilisons curl (par exemple) pour accéder à notre API :

$ curl -H 'Accept: application/json; indent=4' http://127.0.0.1:8000/api/beers/
...
{
    "nom": "Abbaye du Park Blonde",
    "type": "Abbaye, Blonde",
    "degre": "6 %",
    "brasserie": "Brasserie Haacht"
},
{
    "nom": "Abbaye du Park Brune",
    "type": "Abbaye, Brune",
    "degre": "6 %",
    "brasserie": "Brasserie Haacht"
},
{
    "nom": "Abdis Blond",
    "type": "Blonde",
    "degre": "6,5 %",
    "brasserie": "Brasserie Riva"
},
{
    "nom": "Abdis Bruin",
    "type": "Brune",
    "degre": "6,5 %",
    "brasserie": "Brasserie Riva"
}
...

Notez pour finir que Django Rest Framework fournit également une interface web pour consulter votre API :

19 Aug 2015, 00:30

Sérialiser une instance de classe en JSON sous Python

Je me suis récemment confronté pour un projet perso à devoir sérialiser une instance de classe en JSON. J’aurais très bien pu utiliser un autre type de sérialisation, comme Pickle, mais je suis habitué au JSON et je préfère rester sur ce format pour l’ensemble de mes projets.

Pour la serialisation nous devons passer par la méthode JSON.dumps(). A priori rien de bien compliqué pour des types basiques (list, dict, str, bool…), mais les instances de classe ne sont pas serialisables.

Prenons une classe toute simple :

# -*- coding: utf-8 -*- 

class MyClass(object):
    def __init__(self):
        self.var1 = "foo"
        self.var2 = "bar"

Si vous essayez de sérialiser une instance de celle-ci, une exception sera levée :

>>> from myclass import MyClass
>>> import json
>>> obj = MyClass()
>>> json.dumps(obj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.4/json/__init__.py", line 230, in dumps
    return _default_encoder.encode(obj)
  File "/usr/lib/python3.4/json/encoder.py", line 192, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.4/json/encoder.py", line 250, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python3.4/json/encoder.py", line 173, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <myclass.MyClass object at 0x7f37074c8e10> is not JSON serializable

Pour pallier à ce problème, la documentation suggère d’utiliser une sous-classe de json.JSONEncoder et de retourner le résultat attendu dans la méthode default(). Ce qui donne dans notre cas :

class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, MyClass):
            return vars(obj)
        else:
            return json.JSONEncoder.default(self, obj)

On spécifie ensuite cet encoder dans l’argument nommé cls :

>>> json.dumps(obj, cls=MyEncoder)
'{"var1": "foo", "var2": "bar"}'

Ca fonctionne, mais il existe une façon bien plus simple d’arriver au même résultat sans devoir créer d’encoder spécifique. Il suffit simplement d’utiliser l’attribut __dict__ de l’instance. Celui-ci retourne l’ensemble des attributs internes d’un objet sous forme de dictionnaire, et c’est exactement ce que nous souhaitons :

>>> json.dumps(obj.__dict__)
'{"var1": "foo", "var2": "bar"}'

C’est beau Python…

05 Aug 2015, 00:11

Modifier le header par défaut dans docutils

Je travaille actuellement sur un générateur statique de blog qui génère les pages HTML à partir de la syntaxe reStructuredText.

J’utilise pour cela docutils, et par défaut les headers (h1, h2, h3…) sont générés par ordre croissant. Si il détecte un h1, alors un header de niveau moindre deviendra h2, ensuite h3, etc.

Malheureusement dans mon thème, le titre de chaque post n’est pas généré par docutils, mais apparaît directement dans un template en tant que h1. Ce qui fait que chaque titre généré par docutils par la suite sera également un h1.

Pour modifier cela, il faut placer le paramètre initial_header_level à la valeur souhaitée :

parts = publish_parts(file.plaintext, writer_name='html',
                      settings_overrides={'initial_header_level':2,})
return parts['html_body']

De cette façon les premiers headers seront directement générés en h2 (il faut mettre la valeur 3 pour h3, 4 pour h4, …).

03 Aug 2015, 16:32

Créer une commande personnalisée pour Git

Git est un formidable outil de versionning fournissant un nombre incroyable de fonctionnalités. Mais il peut arriver un cas particulier pour lequel aucune commande git n’apporte de solution : c’est pourquoi git propose à l’utilisateur de créer ses propres commandes personnalisées.

Il m’arrive par exemple de vouloir connaître le nombre de commits sur un projet. Pour cela je fais :

$ git log --pretty=oneline | wc -l
132

Ca marche mais c’est un peu long à taper à chaque fois. On peut évidemment faire un alias dans notre fichier ~/.bashrc, mais on peut également créer une commande git qui permette de faire la même chose.

Pour cela on crée un fichier dans /usr/bin, par exemple git-count. Le nom des commandes personnalisées doivent être séparées par des tirets et n’ont pas d’extension. On y place le code souhaité :

#!/bin/bash
git log --pretty=oneline | wc -l

N’oubliez pas de rendre ce fichier éxécutable (chmod +x). La commande est maintenant accessible depuis un repository local :

$ git count
132

23 Jul 2015, 20:09

Hello world

Ceux qui me connaissent savent que ce n’est pas la première fois que je créé un blog, et je me lance donc à nouveau dans la rédaction d’articles.

Mon premier site remonte à plusieurs années : il s’agissait d’un blog proposant des articles sur l’univers de l’Open source, notamment Linux. J’ai ensuite créé un blog perso, parlant de sécurité informatique, et enfin un autre sur le Cloud Computing.

Alors pourquoi un quatrième ? D’abord pour garder une trace de mes recherches. Etant développeur Python, je passe du temps sur Github, StackOverflow ou encore Twitter pour effectuer ma veille et trouver les réponses à mes questions. Je note pas mal de choses dans un dokuwiki local, je me dis que ça ne peut pas faire de mal de partager ces notes.

Ensuite parce que je me suis amusé à développer un générateur statique de blog, et que le meilleur moyen de le tester est de l’éprouver en conditions réelles.

Bref je parlerai sur cet espace de pleins de choses qui me tiennent à coeur dans le domaine de l’IT : développement et sécurité web, Docker, ElasticSearch, Django, Linux, le Cloud computing, mes projets perso, etc… et bien sûr du Python :)

Contrairement aux blogs précédents je ne me mettrai pas trop la pression sur les articles : le rythme des publications sera simplement basé sur mes envies du moment.

A bientôt !