Autoblog de sametmax.com

Ce site n'est pas le site officiel de sametmax.com.
C'est un blog automatisé qui réplique les articles de sametmax.com

Super article invité sur Trio que l’auteur a oublié de titrer 6   Recently updated !

Thu, 14 Jun 2018 07:39:52 +0000 - (source)

Ceci est un post invité de touilleMan posté sous licence creative common 3.0 unported.

C’est bon vous avez cédé à la hype ?

Après un n-ème talk sur asyncio vous avez été convaincu que tout vos sites webs doivent être recordé dans cette techno ? Oui, surtout celui de la mairie de Gaudriole-sur-Gironde avec ses 50 visiteurs/jour, Django ça scalera pas et vous aurez sûrement besoin de websockets à l’avenir.

Et puis là pan ! En commençant à utiliser asyncio on se rend compte que ça va pas être aussi marrant que ce que vous a vendu l’enfoiré de hipster dans son talk avec son exemple de crawler web en 20 lignes :

Je ne parle même pas des soucis ceinture-noir-2ème-dan du genre high-water mark qui vous tomberons dessus une fois l’appli en prod.

Lourd est le parpaing de la réalité sur la tartelette aux fraises de nos illusions…

1 – Pourquoi c’est (de) la merde ?

Pour faire simple asyncio a été pensé à la base comme une tentative de standardisation de l’écosystème asynchrone Python où chaque framework (Twisted et Tornado principalement) était incompatible avec les autres et devait re-créer son écosystème de zéro.

C’était la bonne chose à faire à l’époque, ça a eu beaucoup de succès (Twisted et Tornado sont maintenant compatible asyncio), ça a donné une killer-feature pour faire taire les rageux au sujet de Python 3 et ça a créé une émulsion formidable concernant la programmation asynchrone en Python.
Mais dans le même temps ça a obligé cette nouvelle lib à hériter des choix historiques des anciennes libs : les callbacks.

Pour faire simple un framework asynchrone c’est deux choses :

Concernant le 2ème point, cela veut dire que si on a une fonction synchrone comme ceci :

def listen_and_answer(sock):
    print('start')
    data = sock.read()
    print('working with %s' % data)
    sock.write('ok')
    print('done')

Il faut trouver un moyen pour la découper en une série de morceaux de codes et d’IO.

Il y la façon « javascript », où on découpe à la main comme un compilo déroulerai une boucle :

def listen_and_answer(sock):
    print('start')
 
    def on_listen(data):
        print('working with %s' % data)
 
        def on_write(ret):
            print('done')
 
        sock.write('ok', on_write)
 
    sock.read(on_listen)

Et là j’ai fait la version simple sans chercher à gérer les exceptions et autres joyeusetés. Autant dire que quand un vieux dev Twisted vous dit le regard vide et la voix chevrotante qu’il a connu l’enfer, ne prenez pas ses déclarations à la légère.

Sinon la façon async/await si chère à asyncio :

async def listen_and_answer(sock):
    print('start')
    data = await sock.read()
    print('working with %s' % data)
    await sock.write('ok')
    print('done')

C’est clair, c’est propre, la gestion des exceptions est totalement naturelle, bref c’est du Python dans toute sa splendeur.
Sauf que non, tout ça n’est qu’un putain d’écran de fumée : pour être compatible avec Twisted&co sous le capot asyncio fonctionne avec des callbacks.

Vous vous souvenez de cette sensation de détresse mêlée d’hilarité devant une stacktrace d’un projet Javascript lambda d’où vous ne reconnaissez que la première ligne ? C’est ça les callbacks, et c’est ça que vous avez dans asyncio.

Concrètement le soucis vient du fait qu’une callback n’est rien d’autre qu’une fonction passée telle qu’elle sans aucune information quant à d’où elle vient. De fait impossible pour l’event loop asynchrone de reconstruire une callstack complète à partir de cela.
Heureusement async/await permettent à python de conserver ces informations de fonction appelante ce qui limite un peu le problème avec asyncio.
Toutefois en remontant suffisamment haut on finira toujours avec une callback quelque part. Et vous savez qui a l’habitude de remonter aussi haut que nécessaire ? Les exceptions.

import asyncio
import random
 
async def succeed(client_writer):
    print('Lucky guy...')
    # Googlez "ayncio high water mark" pour comprender pourquoi c'est
    # une idée à la con de ne pas avoir cette methode asynchrone
    client_writer.write(b'Lucky guy...')
 
async def fail(client_writer):
    raise RuntimeError('Tough shit...')
 
async def handle_request_russian_roulette_style(client_reader, client_writer):
    handlers = (
        succeed,
        succeed,
        succeed,
        fail,
    )
    await handlers[random.randint(0, 3)](client_writer)
    client_writer.close()
 
async def start_server():
    server = await asyncio.start_server(
        handle_request_russian_roulette_style,
        host='localhost', port=8080)
    await server.wait_closed()
 
asyncio.get_event_loop().run_until_complete(start_server())

Maintenant si on lance tout ça et qu’on envoie des curl localhost:8080 on va finir avec:

$ python3 russian_roulette_server.py
Lucky guy...
Lucky guy...
Task exception was never retrieved
future:  exception=RuntimeError('Tough shit...',)>
Traceback (most recent call last):
  File "ex.py", line 18, in handle_request_russian_roulette_style
    await handlers[random.randint(0, 3)](client_writer)
  File "ex.py", line 9, in fail
    raise RuntimeError('Tough shit...')
RuntimeError: Tough shit...
Lucky guy...
Task exception was never retrieved
future:  exception=RuntimeError('Tough shit...',)>
Traceback (most recent call last):
  File "ex.py", line 18, in handle_request_russian_roulette_style
    await handlers[random.randint(0, 3)](client_writer)
  File "ex.py", line 9, in fail
    raise RuntimeError('Tough shit...')
RuntimeError: Tough shit...

Le problème saute aux yeux: asyncio.start_server gère sa tambouille avec des callbacks et se retrouve bien embêté quand notre code remonte une exception. Du coup il fait au mieux en affichant la stacktrace et en faisant comme si de rien n’était. C’est peut-être le comportement qu’on attend d’un serveur web (encore que… si aviez configuré logging pour envoyer dans un fichier vous êtes bien baïzay) mais il existe des tonnes de usecases pour lesquels ça pose problème (et de toute façon on n’a vu que la partie émergée de l’iceberg d’emmerdes qu’est la programmation asynchrone).

Bref, si vous voulez en savoir plus, allez lire ce post, d’ailleurs allez lire tous les posts du blog, ce mec est un génie.

2 – Trio, une façon de faire de l’asynchrone

Ce mec en question, c’est Nathaniel J. Smith et il a eu la très cool idée de créer sa propre lib asynchrone pour Python: Trio

L’objectif est simple: rendre la programmation asynchrone (presque) aussi simple que celle synchrone en s’appuyant sur les nouvelles fonctionnalités offertes par les dernières versions de Python ainsi qu’un paradigme de concurrence innovant. Cette phrase est digne d’un marketeux, vous avez le droit de me cracher à la gueule.

Concrètement ce que ça donne:

# pip install trio asks beautifulsoup4
import trio
import asks
import bs4
import re
 
 
# Asks est un grosso modo requests en asynchrone, vu qu'il supporte trio et curio
# (une autre lib asynchrone dans le même style), il faut donc lui dire lequel utiliser
asks.init('trio')
 
 
async def recursive_find(url, on_found, depth=0):
    # On fait notre requête HTTP en asynchrone
    rep = await asks.get(url)
    print(f'depth {depth}, try {url}...')
 
    # On retrouve le corps de l'article grace à beautiful soup
    soup = bs4.BeautifulSoup(rep.text, 'html.parser')
    body = soup.find('div', attrs={"id": 'mw-content-text'})
 
    # On cherche notre point Godwin
    if re.search(r'(?i)hitler|nazi|adolf', body.text):
        on_found(url, depth)
 
    else:
        async with trio.open_nursery() as nursery:
            # On retrouve tous les liens de l'article et relance le recherche
            # de manière récursive
            for tag in body.find_all('a'):
                if tag.has_attr('href') and tag.attrs['href'].startswith('/wiki/'):
                    child_link = 'https://en.wikipedia.org' + tag.attrs['href']
                    # On créé une nouvelle coroutine par lien à crawler
                    nursery.start_soon(recursive_find, child_link, on_found, depth+1)
 
 
async def godwin_find(url):
    results = []
 
    with trio.move_on_after(10) as cancel_scope:
        def on_found(found_url, depth):
            results.append((found_url, depth))
            cancel_scope.cancel()
 
        await recursive_find(url, on_found)
 
    if results:
        found_url, depth = results[0]
        print(f'Found Godwin point in {found_url} (depth: {depth})')
    else:
        print('No point for this article')
 
 
trio.run(godwin_find, 'https://en.wikipedia.org/wiki/My_Little_Pony')

L’idée de ce code est, partant d’un article wikipedia, de crawler ses liens récursivement jusqu’à ce qu’on trouve un article contenant des mots clés.

Au niveau des trucs intéressants:

async with trio.open_nursery() as nursery:
    for tag in body.find_all('a'):
        if tag.has_attr('href') and tag.attrs['href'].startswith('/wiki/'):
            child_link = 'https://en.wikipedia.org' + tag.attrs['href']
            nursery.start_soon(recursive_find, child_link, on_found, depth+1)

En trio, une coroutine doit forcément être connectée à une nurserie. Cela permet deux choses:

Quel intérêt à borner la durée de vie des coroutines ? Si on avait voulu écrire un truc équivalent en asyncio on aurait sans doute utilisé asyncio.gather:

coroutines = [recursive_find(link) for link in links]
await asyncio.gather(coroutines)

Maintenant on fait tourner ce code avec une connection internet un peu faiblarde (au hasard sur la box Orange de Sam ces temps ci…) les ennuis auraient commencé dès qu’une requête http aurait timeout.
L’exception de timeout aurait été récupérée par asyncio.gather qui l’aurait relancé sans pour autant fermer les autres coroutines qui auraient continué à crawler wikipedia en créant des centaines de coroutines (oui recursive_find est un peu bourrin).
De fait si on se place dans le cas d’un code tournant longtemps (typiquement on a un serveur web qui a lancé notre code dans le cadre du traitement d’une requête entrante) on va avoir bien du mal à retrouver l’état ayant mené à ce bordel.

Du coup en trio la seule solution pour avoir une coroutine qui survit à son parent c’est de lui passer une nursery en paramètre:

async def work(sleep_time, nursery):
    await trio.sleep(sleep_time)
    print('work done !')
    # Je vous ai dit qu'une nurserie contient automatiquement un cancel scope ?
    nursery.cancel_scope.cancel()
 
async def work_generator(nursery):
    print('bootstrapping...')
    await trio.sleep(1)
    for sleep_time in range(10):
        nursery.start_soon(work, sleep_time, nursery)
 
async def stop_a_first_work_done():
    async with trio.open_nursery() as nursery:
        await work_generator(nursery)
        print('Waiting for a work to finish...')

Un autre truc cool:

with trio.move_on_after(10) as cancel_scope:
    def on_found(found_url, depth):
        results.append((found_url, depth))
        cancel_scope.cancel()
 
    await recursive_find(url, on_found)

Vu qu’en trio on se retrouve avec un arbre de coroutines, il est très facile d’appliquer des conditions sur un sous-ensemble de l’arbre. C’est le rôle des cancel scope.
Comme pour les nursery, les cancel scope sont des contexts managers (mais synchrone ceux-ci). On peut les configurer avec un timeout, une deadline, ou bien tout simplement les annuler manuellement via cancel_scope.cancel().

Dans notre exemple, on définit un scope dont on sortira obligatoirement au bout de 10s. Pour éviter d’attendre pour rien, on annule le scope explicitement dans la closure appelée quand un résultat est trouvé.
Vu que les nurseries définies à chaque appel de recursive_find se trouvent englobées par notre cancel scope, elles seront automatiquement détruites (et toutes les coroutines qu’elles gèrent avec).

Pour faire la même chose avec asyncio bonne chance:

En plus comme en parlait un mec (décidemment !), la gestion du timeout dans une socket tcp est foireuse, il suffit de recevoir un paquet (et une requête entière peut contenir beaucoup de paquets !) pour que le timeout soit remis à zéro. Donc encore une fois pas de garanties fortes quant à quand le code s’arrêtera.

3 – Eeeeet c’est tout !

Au final la doc de l’api de trio pourrait tenir sur l’étiquette de mon slip: pas de promise, de futurs, de tasks, de pattern Protocol/Transport legacy. On se retrouve juste avec la sainte trinité (j’imagine que c’est de là que vient le nom) async/await, nursery, cancel scope.

Et évidemment maintenant, l’enfoiré de hipster qui vous vend une techno à coup de whao effect avec un crawler asynchrone de 20 lignes c’est moi…

Remarquez si vous préférez la version longue je vous conseil cet excellent article de Nathaniel (je vous ai dit que ce mec était un génie ?).

4 – L’écosystème

C’est là où on se rend compte que asyncio est malgré ses lacunes une super idée: il a suffit d’écrire une implémentation de l’event loop asyncio en trio pour pouvoir utiliser tout l’écosystème asyncio (ce qui inclus donc Twisted et Tornado, snif c’est beau !).

Allez pour le plasir un exemple d’utilisation de asyncpg depuis trio:

import trio_asyncio
import asyncpg
 
 
class TrioConnProxy:
    # Le décorateur permet de marquer la frontière entre trio et asyncio
    @trio_asyncio.trio2aio
    async def init(self, url):
        # Ici on est donc dans asyncio
        self.conn = await asyncpg.connect(url)
 
    @trio_asyncio.trio2aio
    async def execute(self, *args):
        return await self.conn.execute(*args)
 
    @trio_asyncio.trio2aio
    async def fetch(self, *args):
        return await self.conn.fetch(*args)
 
 
async def main():
    # Ici on est dans trio, c'est la fête
 
    conn = TrioConnProxy()
    await conn.init('postgresql:///')
 
    await conn.execute('CREATE TABLE IF NOT EXISTS users(name text primary key)')
 
    for name in ('Riri', 'Fifi', 'Loulou'):
        await conn.execute('INSERT INTO users(name) VALUES ($1)', name)
 
    users = await conn.fetch('SELECT * FROM users')
    print('users:', [user[0] for user in users])
 
 
# trio_asyncio s'occupe de configurer l'event loop par défaut de asyncio
# puis lance le <code>trio.run</code> classique
trio_asyncio.run(main)

En plus de ça trio vient avec son module pytest (avec gestion des fixtures asynchrones s’il vous plait) et Keneith Reitz a promis que la prochain version de requests supporterait async/await et trio nativement, elle est pas belle la vie !


Go to (in asyncio) considered harmful 23

Thu, 07 Jun 2018 07:31:29 +0000 - (source)

Dijkstra était un intellectuel pédant, mais quand il a écrit cette lettre célèbre, il a comme souvent mis le doigt sur un truc fondamental. Et quand l’auteur de Trio, une stack toute neuve concurrente d’asyncio, lui a fait écho 50 ans plus tard, ça a beaucoup discuté sur les mailing lists et les bugs trackers.

Nathaniel J. Smith, le dev susnommé, en a profité pour introduire une nouvelle primitive, actuellement surnommée la nursery, pour répondre au problème. Une idée visiblement tellement bonne que notre Yury préféré a décidé de la porter à asyncio. La boucle d’événements est bouclée, si je puis dire.

Mais une autre chose intéressante en découle : on a mis en lumière la présence d’un goto dans asyncio, et qu’il y a de bonnes pratiques, validées par Guido himself, pour coder avec cette lib pour éviter les douleurs.

What the fuck are you talking about ?

Le problème du goto, c’est que l’instruction permet d’aller de n’importe où à n’importe où. Cela rend le flux du programme très dur à suivre. Pour éviter cela, on a catégorisé les usages clean du goto: répéter une action, changer de comportement en fonction d’un test, sortir d’un algo en cas de problème, etc. Et on en a fait des primitives : les if, les while, les exceptions… Dans les langages les plus modernes, on a carrément viré le goto pour éviter les abus et erreurs. Joie.

Dans asyncio, le “goto” en question se trouve quand on veut lancer des tâches en arrière plan, comme ceci :

import asyncio as aio
loop = aio.get_event_loop()
aio.ensure_future(foo())  # GOTO !
aio.ensure_future(bar())  # GOTO !
loop.run_forever()

Le problème d’ensure_future() est multiple:

En prime run_forever() est un piège à con, car les exceptions qui arrivent dans la boucle sont logguées, mais ne font pas crasher le programme, ce qui rend le debuggage super rude, même avec debug mode activé (dont de toute façon personne ne soupçonne l’existence).

La solution asyncio

import asyncio as aio
loop = aio.get_event_loop()
loop.run_until_complete(aio.gather(foo(), bar())

En plus d’être plus court, les exceptions vont faire planter le programme, la loop s’arrêtera quand les coroutines auront fini leur taff, leur flux a un début et une fin encapsulés par le gather(). Ceci est encore plus visible si on met le même code à l’intérieur d’une coroutine à l’intérieur d’une coroutine à l’intérieur d’une coroutine plutôt qu’à la racine du programme. En effet dans un exemple si simple, on se borne au démarrage et à l’arrêt de la boucle. Mais je suis paresseux.

Donc, c’est la bonne pratique, mais tout le monde ne le sait pas.

Pardon, correction.

Tous les devs Python ne connaissent pas asyncio. Parmi ceux qui connaissent asyncio, une petite partie comprend comme ça marche.

Dans ce lot rikiki, un pouillième sait que c’est la bonne pratique.

En fait, gather() est probablement la fonction la plus importante d’asyncio, et pourtant elle apparaît à peine dans la doc. C’est la malédiction d’asyncio, une lib que tout le monde attendait pour propulser Python dans la league des langages avec frameworks modernes, mais qui commence à peine à devenir utilisable par le commun des mortel en 2018. Et encore.

Il ne faut jamais utiliser ensure_future() à moins de vouloir attacher un callback à la main dessus, ce qui n’est probablement jamais ce que vous voulez à cette époque merveilleuse ou existe async/await. ensure_future() est un goto, gather() est un concept de plus haut niveau.

Mais deux problèmes demeurent…

Contrairement au goto banni de Python, ensure_future() est là, et va rester. Donc n’importe quel connard peut dans un code ailleurs vous niquer profond, et en tâche de fond.

ensure_future() (ou son petit frère EventLoop.create_task()) reste le seul moyen valable pour lancer une tâche, faire quelque chose, lancer une autre tâche, puis enfin faire un gather() sur les deux tâches:

async def grrr():
    task1 = aio.ensure_future(foo())
    # faire un truc pendant que task1 tourne
    task2 = aio.ensure_future(bar())
    # faire un truc pendant que task1 et task2 tournent
    # On s'assure que tout se rejoint à la fin:
    await aio.gather(task1, task2)

Et puis, faire une pyramide de gather() dans tout son code pour s’assurer que tout va bien de haut en bas, c’est facile à rater.

La nursery : la solution de trio

Une nursery agit comme un scope qui pose les limites du cycle de vie des tâches qui lui sont attachées. C’est un gather(), sous stéroide, et avec une portée visuellement claire:

async def grrr():
    async with trio.open_nursery() as nursery:
        task1 = nursery.start_soon(foo)
        # faire un truc pendant que task1 tourne
        task2 = nursery.start_soon(bar)
        # faire un truc pendant que task1 et task2 tournent

Les taches sont garanties, à la sortie du with, de se terminer. Le ensure_future() n’a pas d’équivalent en trio, et donc aucun moyen de lancer un truc dans le vent sans explicitement lui passer au moins une nursery à laquelle on souhaite l’attacher.

Résultat, on ne peut plus faire de goto, et le flux du program est clair et explicite.

Notez que, tout comme if et while ne permettaient rien qu’un utilisateur soigneux de goto ne pouvait faire, la nursery ne permet rien qu’un utilisateur soigneux de ensure_future() ne peut faire. Mais ça force un ensemble de bonnes pratiques.

Évidemment, on peut ouvrir une nursery dans un bloc d’une autre nursery, ce qui permet d’imbriquer différentes portées, comme on le ferait avec un begin() de transaction de base de données. Or, une exception à l’intérieur d’une nursery bubble naturellement comme toute exception Python, et stoppe toutes les tâches de la nursery encore en train de tourner. Alors qu’avec asyncio vous l’avez dans le cul.

En définitive, c’était la pièce manquante. La moitié du boulot avait était faite quand on a introduit un moyen de gérer des tâches asynchrones qui dépendent les unes des autres, en remplaçant les callbacks par un truc de haut niveau : async/await. Il restait la gestion des tâches en parallèle qui se faisait encore selon les goûts et compétences de chacun, mais la nursery va remplir ce vide.

Cela devrait être intégré à asyncio en Python 3.8, soit une bonne année et demie pour ceux qui ont la chance de pouvoir faire du bleeding edge.

Comme certains ne voudront pas attendre, je vous ai fait un POC qui vous montre comment ça pourrait marcher. Mais cette version ne sera jamais utilisée. En effet, elle intercepte ensure_future() (en fait le create_task() sous-jacent) pour attacher son résultat à la nursery en cours, évitant tout effet goto, et ça péterait trop de code existant. Mon pognon est plutôt sur un gros warning émis par Python quand on fait une gotise.

Dernier mot: s’il vous plaît, allez voter pour change le nom de nursery. C’est beaucoup trop long à taper pour un truc qu’on va utiliser tout le temps.


Once you go black, you never go back 17

Wed, 06 Jun 2018 11:16:12 +0000 - (source)

L’indentation obligatoire et l’existence du PEP8 sont pour moi deux features fondamentales de Python, limitant énormément la quantité de code illisible qu’on trouve dans la communauté.

Malgré cela, le reformatage de code reste une tache courante, et nécessaire, mais un gâchis énorme de temps. D’abord il faut décider comment on va formater, ce qui en équipe veut dire débat sur le pire sujet qui soit: le goût. Ensuite il faut mettre en place des configurations de linter (flake8, pylint, etc), et potentiellement l’infra qui va avec (tox, hooks git, CI…).

Pour cette raison, de nombreux outils de formatage automatique ont vu le jour. Le premier a été autopep8, et plus tard yapf de Google.

Mais ces deux outils ont quelques soucis:

Le monde du langage Go a choisi une stratégie différente: la technique du “ta gueule”.

Et aussi: ta gueule

Et aussi: ta gueule

Cette technique subtile et raffinée s’est incarnée dans l’outil Gofmt, qui est fourni par défaut avec go, et n’a AUCUN réglage.

Le résultat, tout le monde a fermé sa gueule et a adopté l’outil.

Est-ce que le formatage est parfait ? Non.

Est-ce qu’il plaît à tout le monde ? Absolument pas.

Est-ce qu’il fait fermer sa putain de gueule à tout le monde afin qu’on puisse enfin retourner à des choses plus importantes comme coder ?

Yes !

Gofmt produit un formatage suffisamment clair et pragmatique, et comme il est fortement ancré dans la communauté, tout le monde est à la même enseigne. Passer d’un code à un autre est facile. Pas de temps perdu à discuter du style ou à tweaker ses linters. Tout le monde lance go fmt (aka go ferme ta …) et on passe à autre chose.

Dernièrement facebook a décidé de faire pareil, et à pondu en open source black (en référence à Henry Ford), un outil de formatage en Python, qui n’a que 2 réglages. Il suit le PEP8, mais évidemment sa propre interprétation, et ne propose rien d’autre.

Black a aussi l’avantage de fournir des diffs assez petits, et surtout, vérifie si l’AST change après un reformatage, et annule le cas échéant, garantissant que le sens de votre code n’est pas altéré.

Est-ce que j’aime toutes les règles de formatages de black ? Non.

Est-ce que regarder sa sortie me donne parfois envie de me bouffer les couilles parce que franchement, qui pense que c’est une bonne idée d’aligner les choses comme ça ? Parfois.

Mais c’est good enough.

Et du coup, l’adoption de black a été très rapide dans la communauté, et il a été appliqué à heroku, requests, tablib, envoy, clint, fabric 2 et pytest. 4000 stars sur github.

Installation

Évidemment, ça se pip install, mais uniquement sur Python 3.6. Black peut checker du code 2.7, mais il lui faut du 3.6 minimum pour exister, donc on l’installe en parallèle. Évidemment, on peut l’intégrer à ST, Vim ou VSCode. Si votre projet utilise un Python different, il faut donc dans les options faire pointer l’exécutable vers l’installation séparée.

Résultat

Dans l’esprit du lien partagé par Seb, créons un générateur de titre de film porno:

 
import random
 
subject_qualifiers = ( "shy", "mature", "busty", "hot", "horny", "ebony", "quiet", "excited", "naughty", "bad", "cheating", "beautifull", "gorgeous", "drunk", "emo", "fat", "chubby", "goth", "lingery wearing", "latex enthousiast", "placid", "energic", 'slutty', 'sweaty', 'curvy', )
 
subjects =(
    'teen',
    'doll',
    'brunette',
    'blonde',
    'midget',
    'milf',
    'bitch',
    'babe',
    'sister',
    'step-mom',
    'vixen',
    'secretary',
    'real estate agent',
    'teacher',
    'student',
    'schoolgirl',
    'cheer leader',
    'asian tourist',
    'babysitter',
    'ex girlfriend',
    'nurse',
    'squirtter',
    'model',
    'granny',
    'furry',
)
 
actions = (
        "recieves anal",
        "get busted",
        "driven to bukakke",
        "taught double penetration",
        "fucked hard",
        'gently chocked',
        'punished',
        'forced into blow job',
        'pounded',
        'creampied',
        'ass raped',
        "eaten",
        "get her pussy wet",
        "shamed",
        "get an orgasm for the first time",
        'lead to loud climax',
        'offered best sex of her life',
        'worn out',
        'cured from boredom',
        'warmed up',
        'loved in and out',
        'generously oiled',
        'shocked and impressed',
        'decieved into giving it',
        'woke up roughly',
        'get sexy massage',
        'ridden to exhaustion',
        'turned into a lavish slave',
        'never submit to torture',
        'rebels against abuses',
        'taken in every possible way',
        'enjoy the 10 inches provided',
)
 
actors = (
        "pawn shop owner",
    "corrupted cop",
    "dirty plumber",
    "big ass nigga",
    "sport coach"
    "her boss",
    "twisted psychiatrist",
    "ripped doctor",
    "crispy fire fighter",
    "smug playboy",
    "skinny geek",
    "eccentric millionaire",
    "airplane pilot",
    "movie star",
    'football team',
    'her big brother',
    'security guard',
    'hairy beast',
    'wasted guitard player',
    'hung indian immigrant',
    'a guy twice her size',
    '17 guys in a row',
    'her ideal man',
    'her secret prince charming',
    'weirdo albinos',
    'muscle giant',
    'the worst cook ever',
    'cable man',
    'more men that she can count',
    'two friendly brothers',
    'enrike strongsteel'
)
 
contexts = (
    "on the beach","in a cheap motel","in the back of a van",
    "in airplane toilets", "for hours", "to pay back her depts",
    "for a stupid mistake", "and it gets better", "and ask for more",
    "because she could", "in exchange for a favor",
    "right next to her boyfriend", "as a reward",
    "hopping to get him back", "caught on security cam", "every monday",
    "in a barn", "but that's not all", 'but she has a secret',
    "and she has a dick too", 'before inviting her friend over',
    'while her father is watching', 'with her ', "while auditing for a role",
    "to get her job back", "for an interview", "in exclusive sex tape",
    "again and again", ", begging to stop", "for a change", "for chrismas",
    "in public", 'in a back alley', "during a concert", 'on her death bed'
)
 
punctuation = ('','!','!!','...')
 
def get_title(subject_qualifiers, subjects, actions, actors, contexts) :
 
 
    qualifier = random.choice(subject_qualifiers)
    subject = random.choice(subjects)
    action = random.choice(actions)
    actor = random.choice(actors)
    context = random.choice(contexts)
 
    return f"{qualifier} {subject} {action} by {actor} {context}" .capitalize()
 
 
if __name__ == "__main__":
    print(get_title(subject_qualifiers = subject_qualifiers, subjects=subjects,
                    actions=actions, actors=actors, contexts=contexts))

Usage:

$ python3.6 porn_title_generator.py
Chubby model loves bukakke by skinny geek during a concert
$ python3.6 porn_title_generator.py
Busty bitch rebel against abuses by security guard but she has a secret
$ python3.6 porn_title_generator.py
Lingery wearing student creampied by weirdo albinos on the beach
$ python3.6 porn_title_generator.py
Horny bitch offered best sex of her life by hairy beast in airplane toilets
$ python3.6 porn_title_generator.py
Emo blonde punished by airplane pilot on the beach
$ python3.6 porn_title_generator.py
Quiet squirtter lead to loud climax by wasted guitard player while auditing for a role
$ python3.6 porn_title_generator.py
Emo babysitter get her pussy wet by football team caught on security cam
$ python3.6 porn_title_generator.py
Busty asian tourist taken in every possible way by muscle giant and she has a dick too
$ python3.6 porn_title_generator.py
Placid milf ass raped by muscle giant in a back alley
Je soupçonne un coup des frères Markov

Je soupçonne un coup des frères Markov

On applique black, zero réglage, usage simplissime:

$ black . # appel recursif, modification in place par défaut

Le résultat.

import random
 
subject_qualifiers = (
    "shy",
    "mature",
    "busty",
    "hot",
    "horny",
    "ebony",
    "quiet",
    "excited",
    "naughty",
    "bad",
    "cheating",
    "beautifull",
    "gorgeous",
    "drunk",
    "emo",
    "fat",
    "chubby",
    "goth",
    "lingery wearing",
    "latex enthousiast",
    "placid",
    "energic",
    "slutty",
    "sweaty",
    "curvy",
)
 
subjects = (
    "teen",
    "doll",
    "brunette",
    "blonde",
    "midget",
    "milf",
    "bitch",
    "babe",
    "sister",
    "step-mom",
    "vixen",
    "secretary",
    "real estate agent",
    "teacher",
    "student",
    "schoolgirl",
    "cheer leader",
    "asian tourist",
    "babysitter",
    "ex girlfriend",
    "nurse",
    "squirtter",
    "model",
    "granny",
    "furry",
)
 
actions = (
    "recieves anal",
    "get busted",
    "driven to bukakke",
    "taught double penetration",
    "fucked hard",
    "gently chocked",
    "punished",
    "forced into blow job",
    "pounded",
    "creampied",
    "ass raped",
    "eaten",
    "get her pussy wet",
    "shamed",
    "get an orgasm for the first time",
    "lead to loud climax",
    "offered best sex of her life",
    "worn out",
    "cured from boredom",
    "warmed up",
    "loved in and out",
    "generously oiled",
    "shocked and impressed",
    "decieved into giving it",
    "woke up roughly",
    "get sexy massage",
    "ridden to exhaustion",
    "turned into a lavish slave",
    "never submit to torture",
    "rebels against abuses",
    "taken in every possible way",
    "enjoy the 10 inches provided",
)
 
actors = (
    "pawn shop owner",
    "corrupted cop",
    "dirty plumber",
    "big ass nigga",
    "sport coach" "her boss",
    "twisted psychiatrist",
    "ripped doctor",
    "crispy fire fighter",
    "smug playboy",
    "skinny geek",
    "eccentric millionaire",
    "airplane pilot",
    "movie star",
    "football team",
    "her big brother",
    "security guard",
    "hairy beast",
    "wasted guitard player",
    "hung indian immigrant",
    "a guy twice her size",
    "17 guys in a row",
    "her ideal man",
    "her secret prince charming",
    "weirdo albinos",
    "muscle giant",
    "the worst cook ever",
    "cable man",
    "more men that she can count",
    "two friendly brothers",
    "enrike strongsteel",
)
 
contexts = (
    "on the beach",
    "in a cheap motel",
    "in the back of a van",
    "in airplane toilets",
    "for hours",
    "to pay back her depts",
    "for a stupid mistake",
    "and it gets better",
    "and ask for more",
    "because she could",
    "in exchange for a favor",
    "right next to her boyfriend",
    "as a reward",
    "hopping to get him back",
    "caught on security cam",
    "every monday",
    "in a barn",
    "but that's not all",
    "but she has a secret",
    "and she has a dick too",
    "before inviting her friend over",
    "while her father is watching",
    "with her ",
    "while auditing for a role",
    "to get her job back",
    "for an interview",
    "in exclusive sex tape",
    "again and again",
    ", begging to stop",
    "for a change",
    "for chrismas",
    "in public",
    "in a back alley",
    "during a concert",
    "on her death bed",
)
 
punctuation = ("", "!", "!!", "...")
 
 
def get_title(subject_qualifiers, subjects, actions, actors, contexts):
 
    qualifier = random.choice(subject_qualifiers)
    subject = random.choice(subjects)
    action = random.choice(actions)
    actor = random.choice(actors)
    context = random.choice(contexts)
 
    return f"{qualifier} {subject} {action} by {actor} {context}".capitalize()
 
 
if __name__ == "__main__":
    print(
        get_title(
            subject_qualifiers=subject_qualifiers,
            subjects=subjects,
            actions=actions,
            actors=actors,
            contexts=contexts,
        )
    )

L’indentation est revue et normalisée vers 4 espaces, les espacements et sauts de ligne sont rééquilibrés (limite de caractères à 88 ), les quotes deviennent toutes ‘”‘. C’est lisible. Le code marche toujours.

Problem solved.


Je fais mon coming out 38

Tue, 29 May 2018 11:48:23 +0000 - (source)

J’utilise VSCode

J’ai vraiment du mal à m’en remettre, et j’ai des proches qui utilisent Vim et ne sont pas toujours encore à l’aise avec l’idée. J’ai refusé d’utiliser Visual Studio à de nombreuses reprises, alors tester son petit frère était déjà un pas osé. Un truc Microsoft. Un truc écrit en Javascript.

Mais bon, j’aime ça, et il faut pas avoir honte de qui on est.

Le fait que ce soit libre et multiplateforme pour un produit Microsoft est surprenant, néanmoins c’est le maintien continu de l’excellent comportement de la team derrière qui est le plus bluffant: respectueux, proche des utilisateurs, sans bullshit…

Le fait que ce soit facile à installer et utiliser pour un projet javascript est surprenant, néanmoins c’est l’excellente performance du produit qui est le plus bluffant: temps démarrage, réactivité du scroll, gestion de gros projets…

Alors j’ai continué à le garder sous le coude, en parallèle à Sublime Text.

Et quelque chose de subtil a changé, chaque jour, sublime dont j’ai pourtant payé la licence, me faisait de moins en moins bander. Je sollicitais de plus en plus VSCode. Jusqu’à ce que ça devienne mon éditeur par défaut.

Oh, ST et moi on se voit toujours. Pour ouvrir un petit fichier vite fait, taper un article, tester un truc.

En revanche dès que c’est un projet, j’ai un éditeur Electron made in Redmond pour ça, et il me rend heureux.

L’ergonomie de la bestiole

Les auteurs de VSCode ont pompé tous les éditeurs les plus populaires, goulûment. Ils ont optimisé le temps de démarrage à mort, et même si on n’a pas la vitesse d’un ST ou d’un Vim, ça reste moult fois plus rapide que la vaste majorité de la concurrence. Pas de splash screen à rallonge et ce moment de doute où on n’est pas sûr d’avoir vraiment cliqué sur le bouton. J’aime bien Jetbrain mais le startup de PyCharm me fout les boules à chaque fois.

Côté apparence, on retrouve des lignes épurées avec peu de boutons, des tabs, la fameuse bird view du code de ST, un Head Up Display, une statut-bar très riche et le “go to anywhere” que tout le monde adore depuis Mate.

La force de VSCode c’est l’expérience de son équipe : ils ont bien compris ce que les utilisateurs faisaient le plus souvent, et l’ont mis à porter de main. Un clic pour faire un split view ou afficher le terminal intégré. Mais pas de fonction “Imprimer”. Une barre latérale donne l’accès à 4 autres modes, un pour la recherche dans tout le projet, un pour git (et rien d’autre), un pour le debuggeur intégré, et un pour installer des extensions.

Une foule de choses sont configurables, avec une interface qui mélange fichiers de config et aide à la saisie. C’est étrange la première fois qu’on met le nez dans “paramètres de l’utilisateur” ou “ouvrir les raccourcis clavier”. Ni vraiment une fenêtre avec des formulaires. Ni vraiment un JSON à éditer à la main. Un peu des deux. Et c’est super bien fait.

Ceci dit, comme les réglages par défaut sont assez sains, un junior n’aura pas à s’en soucier et pourra tout de suite commencer à introduire des bugs dans votre projet.

L’éditeur

Aucune innovation. Aucune tentative de faire différent de la concurrence. C’est du classique, c’est propre, et ça marche. On peut bien entendu choisir entre plusieurs mode de saisie (mode VI, Emarcs, Sublime, etc), mais perso je reste avec le mode original et quelques raccourcis custo.

Derrière, toutes les fonctionnalités modernes sont là: multi-curseur, sélection/recherche incrémentale, snippets (emmet inclus !), complétion des mots les plus utilisés, navigation par symbole, hot exit. L’avantage, c’est que comme VSCode joue la carte de l’interface minimaliste, on n’a pas besoin de connaitre tout ça, et on peut juste commencer à taper, tout en apprenant chaque feature au fur et à mesure de ses progrès. C’est un excellent éditeur pour débutant en ce sens. Mais les powers users qui aiment malgré tout la souris et les onglets y trouveront leur compte.

La coloration syntaxique est irréprochable (heureusement), mais on voit qu’ils ont du faire des concessions. Par exemple au démarrage, seule la partie de votre viewport est colorée. Il faut attendre une à deux secondes sur les gros projets pour que le reste du fichier le soit, histoire de pas freezer tout le bouzin.

Le bon côté de ça c’est que c’est très fluide. Bon évidemment j’ai 8 coeurs et 32Go de RAM. J’ai tenté l’aventure sur une VM avec 2 de rames et un tout petit coeur, et c’est pénible. Au repos avec quelques tabs ouverts, le truc s’engouffre quand même ses 700Mo de mémoire vive. N’oubliez pas que c’est du V8 derrière.

En comparaison ST en bouffe 300, et Vim, heu, LOL.

Intégration Git

L’intelligence de cette feature, c’est qu’ils se sont limités aux opérations simples et courantes. Permettre de naviguer dans l’espace temps ou de lancer son merge --rebase, c’est dur à faire correctement. Donc VSCode n’essaye pas.

Il affiche juste la liste des fichiers qui sont modifiés et/ou en staging, permet de les bouger de l’un à l’autre ou annuler les modifications, et de faire un commit rapidement. Un clic sur un fichier l’ouvre en mode diff avec HEAD. C’est tout.

C’est pas 1% de ce que permet de faire Git.

Mais c’est facilement 69% de mon usage de Git. Du coup c’est super pratique. Combiné avec le terminal intégré, et vous pouvez gérer presque tout le repo sans sortir de l’éditeur.

Le debuggeur

Je ne l’utilise jamais et je préfère ipdb. Pour le moment, en Python, il est trop lent. Les devs JS en disent du bien, vu qu’apparemment il est capable de se connecter directement au navigateur et comprend TypeScript de manière transparente.

La recherche

Rien à dire. C’est rapide. Ça marche. Ça supporte les trucs les plus importants: case insensitive (activé par défaut), regex, in sélection, dans tous les fichiers, filtrés par extension, et tout le bordel. Cliquer sur le résultat ouvre le fichier à la bonne ligne. Pas de modale qui bloque l’UI.

Pas de surprise, donc. Mais pas de mauvaises surprises.

L’indexage est configurable par projet, ce qui est indispensable dès que vous avez quelque chose d’un peu complexe.

La recherche de fichiers par nom est absente puisque ferait doublons avec “Go To Anywhere”.

Intégration des langages

Là, on attaque la partie intéressante. VSCode est neutre dans son traitement des langages, et toutes les features avancées se font donc via des extensions. L’astuce, c’est que l’équipe supporte officiellement certaines extensions, et elles sont donc d’excellente qualité.

Le support de Python est phénoménal. C’est simplement le meilleur après celui de PyCharm (et de pas beaucoup), ce qui n’est pas peu dire, vu que Jetbrain fait probablement des messes noires et des sacrifices à Quetzalcoatl pour obtenir ce résultat.

Python est notoirement difficile à outiller de par son très grand dynamisme.

Mais là, c’est beau.

Pylint est activé par défaut, et flake8 ainsi que mypy sont optionnellement activables. Leurs préréglages sont de bonne qualité, particulièrement celui de mypy qui est normalement inutilisable out of the box. L’éditeur vous prompte pour l’installation quand il détecte qu’ils sont absents, et lance tout ça pour vous.

Tout est configurable par projet, et donc si vous spécifiez un virtualenv pour votre projet (ce qui vous devriez toujours faire), VSCode va détecter que les outils ne sont pas dedans, vous proposer de les installer, et le faire pour vous.

Du coup, bénéficier des types hints, de la détection des erreurs de syntaxes, des variables non déclarées et des imports manquants ou inutiles est beaucoup plus facile que sur n’importe quel compétiteur. Ok, sauf PyCharm. Mais personnellement je l’appelle PyChiderme.

Si VSCode ne supporte pas nativement un outil, il existe probablement une extension pour ça. Par exemple, il y a une extension pour black, qui est à Python ce que Gofmt est à Go, et que j’installe donc maintenant à chaque nouveau projet.

L’intégration de ces outils est excellente :

VScode m’a même surpris à détecter mes tests unitaires, m’a proposé d’installer pytest puis de lancer tout ça.

Cependant, pour vraiment parler de l’intégration de Python dans VSCode, il me faut mentionner IntelliSense. C’est un terme marketing inventé par MS pour caractériser toutes les fonctionnalités autour de la compréhension que l’éditeur à du code, et des opérations qu’il propose dessus.

Ok, ok, c’est un mot 100% bullshit.

Mais bordel, ça marche.

La complétion du code est excellente, et marche sans aucun réglage. Avec la lib standard bien entendu, mais aussi avec votre code, et toutes les libs installées dans votre virtualenv (si vous avez précisé le chemin vers ledit env dans les settings du projet, of course, il est pas devin).

VSCode affiche les docstrings, les params et propose d’aller à la définition de n’importe quoi en un clic.

Et si comme moi vous avez passé un temps fou à essayer d’obtenir le même résultat sous ST/Vim/Whatever en chargeant what mille plugins et en changeant 600 valeurs de configs, vous comprendrez que c’est juste, topissime.

Quelques infos

Les réglages de VSCode de base sont bons. C’est vraiment une partie de ce qui fait la force du projet: moins de bordel à faire soi-même. Mais comme il est bien configurable, il ne faut pas s’en priver. Quelques trucs que je fais toujours:

Installer une police avec des ligatures

Genre Fira-Code.

Et activer les settings:

    "editor.fontFamily": "'Fira Code', 'Droid Sans Mono', 'Courier New', monospace, 'Droid Sans Fallback'",
    "editor.fontLigatures": true,

Exclure plein de fichiers

J’ai pas du tout envie que “Go to anywhere”, la recherche des fichiers ou l’indexage git charge des trucs inutiles. Donc j’ai des settings de base de nazi:

    "files.exclude": {
        "**/.git": true,
        "**/.svn": true,
        "**/.hg": true,
        "**/.DS_Store": true,
        "**/dist": true,
        "**/build": true,
        "**/env/**": true,
        "**/venv/**": true,
        "**/virtualenv/**": true,
        "**/node_modules": true,
        "**/bower_components": true,
        "**/vendors": true,
        "**/__pycache__": true,
        "**/**/*.pyc": true
    },
    "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/node_modules/**": true,
        "**/build/**": true,
        "**/dist/**": true,
        "**/env/**": true,
        "**/venv/**": true,
        "**/virtualenv/**": true,
        "**/bower_components/**": true,
        "**/vendors/**": true,
        "**/__pycache__": true,
        "**/**/*.pyc": true
    },

Mes settings par projet sont généralement encore plus restrictifs.

Je change les params de zoom

 "window.zoomLevel": 2,
 "editor.mouseWheelZoom": true,
 "editor.fontSize": 10,

Je vire la télémétrie

Je suis pas sous Windows 10, merde.

    "telemetry.enableCrashReporter": false,
    "telemetry.enableTelemetry": false,

Je mets des barres verticales

    "editor.rulers": [
        79, # PEP8
        88, # Black
        120 # Javascript
    ],

Ergonomie perso

    "editor.renderWhitespace": "none", # overridé par projet
    "editor.renderIndentGuides": true,
    "editor.minimap.enabled": true,
    "editor.minimap.renderCharacters": true,
    "editor.autoIndent": true,
    "window.restoreWindows": "all",
    "window.openFoldersInNewWindow": "on",
    "editor.acceptSuggestionOnEnter": "off",
    "editor.tabCompletion": true,
    "emmet.triggerExpansionOnTab": true,

Pour un Python heureux

    "python.venvPath": "~/.local/share/virtualenvs/",
    "python.linting.mypyEnabled": true,
    "python.linting.enabled": true,
    "python.pythonPath": "/usr/bin/python3.6", # je l'override dans les settings de projet
    "black.path": "/home/user/.local/bin/black", # black a besoin de Python 3.6
    "python.formatting.provider": "none", # pour black
    "editor.formatOnPaste": true,
    "files.associations": {
        ".pylintrc": "ini"
    },
    "python.linting.flake8Enabled": true,
    "python.unitTest.pyTestEnabled": true,
    "python.linting.pylintEnabled": true,

J’installe généralement ces extensions

Astuces utiles

VSCode vient avec les raccourcis traditionnels des éditeurs graphiques: ctrl + F pour rechercher, ctrl + shift + f pour rechercher dans le projet, ctrl + d pour la sélection incrémentale, ctrl + p pour le “go to anywhere”, ctrl + shift + p pour le head up display, ctrl + s pour sauvegarder, ctrl (+shift) pour se balader (sélectioner) de mot en mot, etc.

Il possède aussi quelques trucs sympas dont on parle moins dans les tutos:

Un snippet perso que je rajoute également (dans $HOME/.config/Code/User/snippets/python.json):

   "wrap_in_try_except": {
        "prefix": "try",
        "body": [
          "try:",
          "\t${0}${TM_SELECTED_TEXT}",
          "except ${1:Exception}:",
          "\t${2:import pdb; pdb.set_trace()}"
        ],
        "description": "Wrap in try/except"
    },

Ça permet de sélectionner un truc, de taper try puis tab et avoir le tout wrappé dans un try/except.

Le futur

Depuis sa sortie, l’éditeur est en constante amélioration. Les mises à jour sont toujours une excellente surprise, avec des tas de goodies, y compris dans les extensions.

Mais là, dans la dernière version instable (qui a la bonne idée de ne pas overrider la stable à l’installation), VSCode vient avec une preview de l’édition collaborative. Genre Google doc, mais pour le code, et dans tout l’éditeur.

La partie chiante, c’est qu’il faut un compte (Microsoft évidemment), et donc que ça passe par leurs serveurs.

La partie amazing par contre, c’est que ça envoie du poney nucléaire. L’ouverture des onglets, l’écriture, le scroll… Tout se synchronise proprement. Si on décide de faire sa vie, VSCode désynchronise la navigation, et permet à tout le monde de travailler en parallèle sur le projet (et même optionnellement donner accès à son terminal). Si on veut de nouveau voir la navigation de l’autre, on peut demander de le suivre à nouveau, et pouf, on suit ce qu’il fait en live.

Testé avec des clients à des milliers de borne. C’est bluffant.

Vous l’avez compris


Trier un CSV de 5 Go 15

Mon, 14 May 2018 08:26:12 +0000 - (source)

Marrant, j’ai jamais eu autant de RAM, et j’ai jamais eu autant de problèmes de RAM. On est en train de faire un bon dans inefficacité des programmes, et ça va pas aller en s’arrangeant entre docker, electron et nos chers navigateurs. Une grosse app Python peut devenir assez velue aussi niveau mémoire, même quand on n’est pas un boulet comme moi.

Et justement, en relisant un célèbre post de Guido sur la réponse à la blague “comment trier un million d’entiers avec 2M de Ram”, j’ai réalisé 2 choses:

Or c’est un peu ma raison d’être, si vous voulez, de prendre les trucs cools mais imbitables et les rendre utilisables.

Aujourd’hui, donc, on va trier un CSV de 5Go en Python. Ça parle plus qu’un fichier de nombres, et puis ça évite d’expliquer le module array qui est juste une optimisation.

J’ai pris mes petites mimines, et j’ai pondu un CSV qui contient 63Mo de:

A,01/02/2016,2
A,13/07/2011,1
B,24/01/1996,3
C,30/12/1999,1
D,13/07/2011,3
D,01/02/2016,5
E,24/01/1996,4
F,30/12/1999,1
G,13/07/2011,4
H,01/02/2016,4
I,01/02/2016,5
I,13/07/2011,2
A,01/02/2016,2
A,13/07/2011,1

En copier/coller.

Puis j’ai lancé un script pour dupliquer le bébé et jusqu’à atteindre 5,9 Go de taille:

with open('data.csv') as infile:
    with open('data2.csv', 'w') as outfile:
        for x in range(100):
            outfile.write(infile.read())
            infile.seek(0)
Si jamais vous doutiez que je vous aime...

Si jamais vous doutiez que je vous aime…

395366400 lignes. Jusqu’ici tout va bien.

Maintenant, supposons qu’on veuille trier sur la date. Si vos souvenirs en Python sont exacts (ou si vous avez lu notre super article sur Ordonner en Python), vous savez que la solution naïve est de loader le fichier cash pistache, puis d’appeler dessus sorted() avec le paramètre key.

D’abord, il faut choisir le callback à passer à key, c’est à dire la fonction qui va être exécutée pour chaque ligne pour extraire la date de la ligne et permettre ainsi à sorted() de comparer avec les autres dates.

>>> str_date = "A,01/02/2016,2".split(',')[1] # récupère la date uniquement
>>> str_date
'01/02/2016'
>>> ''.join(str_date.split('/')[::-1]) # on inverse la date avoir une valeur ordonnable
'20160201'

On en fait une fonction:

def extract_date(ligne):
    str_date = ligne.split(',')[1]
    return ''.join(str_date.split('/')[::-1])

Ensuite on a juste à ouvrir le fichier, trier les lignes, et sauvegarder tout ça dans l’autre fichier:

with open('data2.csv') as infile:
    with open('sorted_data.csv', 'w') as outfile:
        # on fait le tri
        sorted_lines = sorted(infile, key=extract_date)
        # on ecrit les lignes triées dans le nouveau fichier
        outfile.writelines(sorted_lines)

Easy money, double poney.

Enfin avec un fichier de quelques Mo. Parce que si vous faites ça dans un fichier de 5,9 Go, votre RAM va vomir comme une pom pom girl anorexique.

sorted() sur un disque complet, illustré

sorted() sur un disque complet, illustré

Comment résoudre ce problème ?

Et bien en faisant tout pareil, mais avec des petits morceaux !

import heapq
 
from tempfile import TemporaryFile
from itertools import islice
 
# On garde notre fonction key
def extract_date(ligne):
    str_date = ligne.split(',')[1]
    return ''.join(str_date.split('/')[::-1])
 
# Liste de tous les fichiers temporaires avec les lignes triées
sorted_temp_files = []
 
with open('data2.csv') as infile:
    progress = 0
    while True:
        # On lit seulement 3000000 lignes sur 395366400 à chaque tour de boucle
        lines = list(islice(infile, 3000000))
 
        if not lines:  # plus de ligne ? On sort
            break
 
        # On affiche où on en est
        print("{:.2f}%".format(progress))
        progress += (3000000 / 395366400 * 100)
 
        # On tri les lignes, comme avec sorted() mais sur place. 
        # Ça évite de dupliquer les données en mémoire.
        lines.sort(key=extract_date)
 
        # On crée un fichier temporaire qui va contenir les 3000000 lignes
        # triées
        f = TemporaryFile(mode="r+")
        f.writelines(lines)
 
        # On rembobine le fichier pour pouvoir relire le contenu plus tard
        f.seek(0)
 
        # On balance le fichier dans la liste des fichiers triés
        # L'objet fichier hein. Pas le chemin du fichier. C'est pour ça qu'on
        # a fait .seek(0) avant. On va en avoir besoin plus bas.
        sorted_temp_files.append(f)
 
    # Toute la magie se fait là.
    # On utilise heapq.merge(), qui prend en paramètre plein de trucs qu'on 
    # vient de trier, et permet de se balader dedans comme si c'était un seul
    # gros truc trié, mais sans tout charger en mémoire.
    # En gros il regarde les premières valeurs de tous les itérables, les compares,
    # puis fait retourne un générateur qui yield tout dans l'ordre 
    with open('sorted_data.csv', 'w') as outfile:
        for ligne in heapq.merge(*sorted_temp_files, key=extract_date):
            outfile.write(ligne)

Au lieu de charger les 5 Go en mémoire, on plafonne à 400 Mo. Une raison de plus d’apprendre à utiliser les générateurs.

Sametmax, c'est de la bombe

Sametmax, c’est de la bombe

Alors évidemment, c’est long. Y a genre 40 minutes de traitement. Moins si on utilise pypy et qu’on accepte de up la conso RAM. On code pas en Rust :)

Si vous avez besoin de perfs et que Python reste votre outil de prédilections, 3 solutions restent à votre disposition:

– Prétraiter le fichier en lui rajoutant une première colonne qui contient un timestamp. Ca c’est super rapide à trier.
– Utiliser un truc de numpy ou pandas comme np.memmap().sort(kind=’merge’). C’est du C, donc ça speed.
Acheter de la ram et tout trier en mémoire avec la solution 1.

EDIT:

Un lecteur m’a alpagué sur twitter pour me dire qu’on peut faire ça plus vite et plus facilement avec sort. Oui, évidement.

L’exercice est académique, il est simplifié pour vous permettre de comprendre heapq.merge(). En réalité, on aura vraiment besoin de ça que pour un cas complexe. Exemple, vous lisez un flux de log une socket et vous checkez les adresses IP d’abord pour filtrer celles de la liste noire, puis pour les faire matcher une zone géographique et vous triez tout ça et vous le passez à la suite du pipeline. Et le tout doit marcher sous Linux et Windows Server avec juste la lib standard parce que diab est de mauvais poil.

Évidement que si vous voulez juste trier un CSV vous n’allez pas coder un script python de 30 lignes.


13558 Go de rames 25

Thu, 15 Mar 2018 06:01:04 +0000 - (source)

J’ai une tache celery qui me génère des données de test. Elle est lancée toutes les 5 minutes pour simuler le crawling d’un site qui popule une base de données, le tout piloté par l’ORM django, puis dumpé dans redis.

Jusqu’ici tout va bien.

Mais après un certain temps, ma machine rame, puis se frise.

Et j’ai du mal à y croire. J’ai 8 coeurs, un 32 Go de mémoire vive, un SSD d’un putain de To. On va pas me la faire à l’envers, c’est pas un def tout moisi qui va me faire trembler les genoux. C’est forcément un de ces cons de wallets que j’ai laissé ouvert, encore codé par un nantais ça !

Mais après une enquête minutieuse, qui a consisté en subtilement killer tous mes processus un par un avec echo "douceur" | sed s/c/l, le constat est là.

Eukekaca

Eukekaca.

Cette fonction est responsable:

def generate_fake_stats(x=1000):
 
    cur_stats = {
        s.currency.short_code: s
        for s in CurrencyMarketStatsFactory.build_batch(x)
    }
 
    mn_stats: Dict[str, MNMarketStats] = {
        s.masternode.coin.short_code: s
        for s in MNMarketStatsFactory.build_batch(x)
    }
 
    stats = []
 
    for code, mns in mn_stats.items():
 
        cstats: CurrencyMarketStats = cur_stats.get(code)
 
        if not cstats:
            continue
 
        dollar_value = float(cstats.dollar_value),
        collateral = mns.masternode.collateral
 
        stats = {
            'created': int(mns.created.timestamp()),
            'name': cstats.currency.name,
            'code': code,
            'title': f'{cstats.currency.name} ({code})',
            'marketcap': dollar_value * cstats.supply,
            'dollar_value': dollar_value,
            'change_rate': cstats.change_rate,
            'volume': float(cstats.volume),
            'supply': cstats.supply,
            'roi': mns.roi,
            'mn_worth': collateral * dollar_value,
            'node_count': mns.node_count,
            'required_coins': collateral,
        }
 
    RedisClient.get_instance().jset('marketstats', stats)
    return stats

Il m’a fallu une bonne heure pour trouver ma connerie. J’ai changé plein d’options, mis DEBUG sur False, limité la mémoire de Redis, etc.

Mais non, c’était mon code. Qui générait au bas mot 847390982*1000*16 octets de données, soit 1,355825571×10¹³ pour mes objets Python.

J'ai un nouveau mapping clavier qui permet de n'utiliser qu'un doigt

J’ai un nouveau mapping clavier qui permet de n’utiliser qu’un doigt

Il y en a un peu plus madame, je vous le mets quand même ?

Nan parce que 13 To pour une pov liste de dicos, c’est la boucherie.

Alors, sachant que je vous ai éliminé la recherche des causes parallèles et que vous savez que le bug est de ma (grande et stupide) faute, saurez-vous trouver dans ces lignes le d20 qui tombe sur un 1 à tous les jets ?

Si vous ne trouvez pas, la réponse dans quelques jours.

Explain all the humans !

Explain all the humans !

EDIT pour la réponse:

Comme plusieurs personnes l’ont compris, c’est la virgule sur:

    dollar_value = float(cstats.dollar_value),

Qui est responsable de tout ce malheur.

En effet plus loin on fait:

    'mn_worth': collateral * dollar_value,

Ce qui, au lieu de multiplier un entier par un float, multiplie un entier par un tuple. En python, c’est légal, et ça donne ça:

>>> 10 * (7808979.8989,)
(7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989)

Si collateral est élevé, ce qui est ici mon cas, ça fait de très gros tuples, et le tout dans une boucle.


Do you rotate ? 2

Mon, 05 Mar 2018 03:29:27 +0000 - (source)

L’explosion de la taille des logs a tué plus d’un serveur. Il est amusant de se dire que c’est du texte qui peut mettre à genoux un serveur de streaming vidéo avec 32 coeurs, 2To de SSD et 64 Go de RAM. Mais un bon access log non contrôlé sous nginx ou même le log de debug du wallet de la coin smartcash, ça peut prendre par surprise.

Le module logging de Python a d’excellentes options pour gérer la taille des logs, notamment le RotatingFileHandler.

Sous Linux, c’est logrotate qui fait le boulot pour le système. C’est performant, ça marche super bien, c’est hyper simple à utiliser, et c’est dispo partout.

Bref, en prod, un petit vi sur /etc/logrotate.d/ton_projet et pouf, on peut demander une rotation en 10 lignes:

/var/www/ton_projet/*.log
{
    size 100K
    daily
    rotate 5
    missingok
    notifempty
}

Y a rien à activer, un cron lance déjà logrotate tous les jours.

Par contre, si votre process utilise systemd pour démarrer, inutile de logger manuellement. Tout ce qui est sur la sortie standard est automatiquement loggué avec rotation et accessible avec un petit journalctl -u ton_process.service --since today.

Bref, attendez encore un peu avant de rajouter logstash à votre stack.


Introduction aux extensions Python avec CFFI 16

Wed, 07 Feb 2018 21:58:49 +0000 - (source)

Ceci est un post invité de Realitix posté sous licence creative common 3.0 unported.

Préambule

Vous avez réalisé une analyse de votre code et vous avez un bottleneck ?
Vous souhaitez utiliser une bibliothèque bas niveau (C/C++/Rust) ?

Pas de problème, dans cet article, je vais vous expliquer les différentes solutions et pénétrer en profondeur dans la plus charmante d’entre elles, son petit nom: CFFI.

Sam&Max me faisant l’honneur d’accepter mon article, je vais suivre la guideline du site avec un langage détendu et beaucoup d’exemples.

Qu’est-ce qu’une extension Python ?

Guido, pendant l’acte créateur, n’a pas oublié une chose importante: les extensions Python !
Une extension Python est un module compilé pouvant être importé dans votre code Python.
Cette fonctionnalité est très puissante: cela vous permet d’utiliser un langage bas niveau (et toutes ses capacités) pour créer un module Python.
Vous utilisez très probablement des extensions Python dans vos projets sans le savoir.
Par exemple, si vous souhaitez embarquer la bibliothèque de calcul physique Bullet, vous pouvez le faire au sein d’une extension Python.

Il y a deux points à différencier:

En y réfléchissant bien, créer un code performant revient à créer une bibliothèque tierce et l’importer au sein de l’interpréteur.

Ça laisse rêveur, alors comment fait-on ?

Plusieurs solutions:

  1. L’API C de CPython. Y’en a qu’ont essayé, ils ont eu des problèmes…
    En utilisant cette méthode, vous aurez accès à tout l’interpréteur Python et vous pourrez tout faire… mais à quel prix ?
    Je ne vous recommande pas cette approche, je me suis cassé les dents pendant 4 mois dessus avec un succès mitigé.
  2. Cython est une bonne solution mais plus orienté sur l’optimisation de code.
  3. CFFI: le saint Graal, alléluia!

CFFI: Première mise en bouche

CFFI va vous permettre de créer des extensions Python mais pas que…
Tout comme le module ctypes, il va permettre d’importer une bibliothèque dynamique au runtime.
Si vous ne savez pas ce qu’est une bibliothèque, c’est par ici.

CFFI va donc vous permettre:

Par rapport à ctypes, CFFI apporte une API pythonic et légère, l’API de ctypes étant lourde.
Par rapport à l’API C de CPython… Ha non! Je n’en parle même pas de celle-là!

J’ai dit qu’il y aurait beaucoup d’exemples, alors c’est parti !

D’abord, on installe le bouzin, il y a une dépendance système avec libffi, sur Ubuntu:

sudo apt-get install libffi-dev python3-dev

Sur Windows, libffi est embarquée dans CPython donc pas de soucis.
Ensuite, on conserve les bonnes habitudes avec le classique:

pip install cffi

Je vous conseille d’utiliser un virtualenv mais ce n’est pas le sujet!

Les trois modes

Il y a trois moyens d’utiliser CFFI, comprenez bien cela car c’est la partie tricky:

  1. Le mode ABI/Inline
  2. Le mode API/Out-of-line
  3. Le mode ABI/Out-of-line

On a déjà évoqué les modes ABI et API, mais je n’ai pas encore parlé de Inline et Out-of-line.
CFFI utilise une phase de “compilation” pour parser les header C. Ce n’est pas une vraie compilation mais une phase de traitement qui peut être lourde.
Le mode Inline signifie que ce traitement va être effectué à l’import du module alors que Out-of-line met en cache ce traitement à l’installation du module.

Evidemment, le mode API/Inline ne peut pas exister puisque le mode API impose une phase de “vraie” compilation.

Le mode ABI/Inline

# On commence par import le module cffi qui contient la classe de base FFI
from cffi import FFI
 
# 1 - On instancie l'object FFI, cet objet est la base de cffi
ffi = FFI()
 
# 2 - On appelle la méthode cdef.
# Cette méthode attend en paramètre un header C, c'est à dire
# les déclarations des fonctions C qui seront utilisées par la suite.
# CFFI ne connaîtra que ce qui a été déclaré dans le cdef.
# La puissance de CFFI réside dans cette fonction, à partir d'un header C,
# il va automatiquement créer un wrapper léger.
# A noter: le code dans cdef ne doit pas contenir de directive pré-processeur.
# Ici, on déclare la fonction printf appartenant au namespace C
ffi.cdef("""
    int printf(const char *format, ...);
""")
 
# 3 - On charge la bibliothèque dynamique
# dlopen va charger la biliothèque dynamique et la stocker dans la variable nommée cvar.
# L'argument passé est None, cela demande à cffi de charger le namespace C.
# On peut ici spécifier un fichier .so (Linux) ou .dll (Windows).
# Seul ce qui a été déclaré dans cdef sera accessible dans cvar.
cvar = ffi.dlopen(None)

Comme vous pouvez le voir dans ce bout de code, CFFI est très simple d’utilisation, il suffit de copier le header C pour avoir accès aux fonctions de la bibliothèque.
A noter: si les déclarations dans le cdef ne correspondent pas aux déclarations présentes dans la bibliothèque (au niveau ABI), vous obtiendrez une erreur de segmentation.

Le mode API/Out-of-line

Pour bien comprendre ce mode, nous allons implémenter la fonction factorielle.

# Comme pour le mode ABI, FFI est la classe principale
from cffi import FFI
 
# Par convention, en mode API, on appelle l'instance ffibuilder car le compilateur va être appelé
ffibuilder = FFI()
 
# En mode API, on utilise pas dlopen, mais la fonction set_source.
# Le premier argument est le nom du fichier C à générer, le 2e est le code source.
# Ce code source va être passé au compilateur, il peut donc contenir des directives pré-processeur.
# Dans l'exemple, je passe directement le code source mais en général, on va plutôt ouvrir le fichier avec open().
ffibuilder.set_source("_exemple", """
    long factorielle(int n) {
        long r = n;
        while(n > 1) {
            n -= 1;
            r *= n;
        }
        return r;
    }
""")
 
# Comme pour le mode ABI, on déclare notre fonction avec la méthode cdef.
ffibuilder.cdef("""
    long factorielle(int);
""")
 
 
# Enfin, on va appeler la méthode compile() qui génère l'extension en 2 étapes:
# 1 - Génération d'un fichier C contenant la magie CFFI et notre code C
# 2 - Compilation de ce fichier C en extension Python
if __name__ == "__main__":
    ffibuilder.compile(verbose=True)

Après éxécution du script, voici ce que l’on voit dans le terminal:

generating ./_exemple.c  -> Étape 1: Génération du fichier C
the current directory is '/home/realitix/test'
running build_ext
building '_exemple' extension  -> Étape 2: Génération de l'extension
x86_64-linux-gnu-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -g -fdebug-prefix-map=/build/python3.6-sXpGnM/python3.6-3.6.3=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/home/realitix/venv/py36/include -I/usr/include/python3.6m -c _exemple.c -o ./_exemple.o
x86_64-linux-gnu-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -g -fdebug-prefix-map=/build/python3.6-sXpGnM/python3.6-3.6.3=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 ./_exemple.o -o ./_exemple.cpython-36m-x86_64-linux-gnu.so

Et vous pouvez trouver l’extension Python `_exemple.cpython-36m-x86_64-linux-gnu.so`.
Étudions le module généré, dans un interpréteur Python:

>>> from _exemple import ffi, lib
>>> dir(ffi)
['CData', 'CType', 'NULL', 'RTLD_DEEPBIND', 'RTLD_GLOBAL', 'RTLD_LAZY', 'RTLD_LOCAL', 'RTLD_NODELETE', 'RTLD_NOLOAD', 'RTLD_NOW', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'addressof', 'alignof', 'buffer', 'callback', 'cast', 'def_extern', 'dlclose', 'dlopen', 'errno', 'error', 'from_buffer', 'from_handle', 'gc', 'getctype', 'init_once', 'integer_const', 'list_types', 'memmove', 'new', 'new_allocator', 'new_handle', 'offsetof', 'sizeof', 'string', 'typeof', 'unpack']
>>> dir(lib)
['factorielle']

Les modules générés par CFFI contiennent 2 objets ffi et lib.

Ça vous dit d’utiliser notre extension avec un petit test de performance ? Allons y !

import time
from contextlib import contextmanager
 
# On import lib qui contient notre fonction factorielle
from _exemple import lib
 
# On créé l'équivalent de notre fonction C en Python
def py_factorielle(n):
    r = n
    while n > 1:
        n -= 1
        r *= n
    return r
 
# Un petit contextmanager pour mesurer le temps
@contextmanager
def mesure():
    try:
        debut = time.time()
        yield
    finally:
        fin = time.time() - debut
        print(f'Temps écoulé: {fin}')
 
def test():
    # On va réaliser un factorielle 25 un million de fois
    loop = 1000000
    rec = 25
    # Version Python
    with mesure():
        for _ in range(loop):
            r = py_factorielle(rec)
    # Version CFFI
    with mesure():
        for _ in range(loop):
            r = lib.factorielle(rec)
 
if __name__ == '__main__':
    test()

Le résultat sur ma machine:

Temps écoulé: 1.9101519584655762
Temps écoulé: 0.13172173500061035

La version CFFI est 14 fois plus rapide, pas mal !
CFFI permet de faire vraiment beaucoup de choses simplement, je ne vous ai montré que la surface afin de vous donner envie d’aller plus loin.

Où trouver des ressources

A savoir que CFFI a été créé par Armin Rigo et Maciej Fijalkowski, les deux créateurs de Pypy.
Toutes les extensions créées avec CFFI sont compatibles avec Pypy !

Conclusion

J’espère que cette introduction vous a plu. Si les retours sont bons, je pourrai m’atteler à un tuto plus conséquent.
Vive Python !

Si vous avez des remarques, n’hésitez pas à me le faire savoir: @realitix sur Twitter


Les critiques des ORM sont à côté de la plaque 38

Fri, 29 Dec 2017 10:31:05 +0000 - (source)

En ce moment, y a deux modes. Dire que les cryptomonnaies c’est génial, et dire que les ORM c’est de la merde.

Durant les derniers articles, je pense qu’on vous a assez parlé de crypto, donc on va parler des ORM.

Ou ORMs. Je sais jamais si les acronymes s’accordent.

Rappel: qu’est-ce qu’un ORM ?

Si vous avez lu le titre de l’article et que votre sang n’a fait qu’un tour, vous pouvez passer tout de suite à la partie suivante, puisqu’on va commencer par les révisions.

Un ORM, pour Object-relational Mapping, est une bibliothèque qui permet de décrire son modèle de données, et d’utiliser cette description pour faire le lien entre votre application et une base de données relationnelle. Dans sa forme la plus courante, l’ORM fournit les outils pour générer des requêtes SQL – sans écrire du SQL – et les exécuter, mais présente également les résultats sous forme d’objets du langage dans lequel il est écrit.

Par exemple, en Python, je peux faire de la base de données à la mano (sans ORM):

import sqlite3
 
# Création de la base
with sqlite3.connect('db.sqlite') as conn:
 
    # On se positionne sur la base
    c = conn.cursor()
 
    # Créer la table
    c.execute('''
        CREATE TABLE product (
            name text,
            price real
        )
    ''')
 
    # Insertion de données en base
    c.execute("INSERT INTO product VALUES ('Pizza', 5000)")
    c.execute("INSERT INTO product VALUES ('Love', 150)")
    c.execute("INSERT INTO product VALUES ('Nessie', 3.5)")
 
    # Sauvegarde des changements
    conn.commit()
 
    # Lecture de toute la base:
    for row in c.execute('SELECT * FROM product'):
        print(type(row), ':', *row)

Ce qui va me sortir:

python3 db.py
< class 'tuple' > : Pizza 5000.0
< class 'tuple' > : Love 150.0
< class 'tuple' > : Nessie 3.5

Et voici la même chose avec l’ORM peewee (apres pip install peewee :)):

import peewee as pw
 
# On créé la base
db = pw.SqliteDatabase('product2.db')
 
# Description de la table.
class Product(pw.Model):
 
    name = pw.CharField()
    price = pw.FloatField()
 
    class Meta:
        database = db
 
# Connection et création de la table
db.connect()
db.create_tables([Product])
 
# Insertion de données en base
Product.create(name='Pizza', price=5000)
Product.create(name='Love', price=150)
Product.create(name='Nessie', price=3.5)
 
for p in Product.select():
    print(type(p), ':', p.name, p.price)
 
db.close()

Ce qui sort:

python3 db.py
< class '__main__.Product' > : Pizza 5000.0
< class '__main__.Product' > : Love 150.0
< class '__main__.Product' > : Nessie 3.5

A priori, c’est la même chose, mais avec un style différent. Le premier exemple favorise l’écriture du SQL à la main, fait les requêtes explicitement et récupère les résultats sous forme de tuples. Le second a une surcouche, l’ORM, qui implique que tout est décrit en Python. Le SQL est généré sous le capot, et on récupère les résultats sous forme d’objets Product.

Il existe de nombreux ORM en Python, les plus populaires étant Peewee (pour les petits besoins), SQLAlchemy (pour les gros projets) et l’ORM de Django (ben, si vous utilisez Django ^^). Évidemment le concept des ORM n’est pas particulièrement lié à Python, et on en retrouve dans tous les langages, avec des variations de styles et de fonctionnalités.

Que reproche-t-on aux ORM ?

Avant de répondre aux détracteurs, listons d’abord leurs arguments.

Et vous savez quoi ?

Ils ont raison.

Heu ?

Toutes ces critiques sont parfaitement justifiées.

Sauf qu’elles passent complètement à côté de la problématique.

Les ORM ne servent pas à éviter d’écrire du SQL.

Ça, c’est vaguement un effet de bord.

Ils servent à créer une API unifiée inspectable, une expérience homogène, un point d’entrée unique, un socle de référence explicite et central, pour le modèle de données.

Une fois qu’on a passé pas mal de temps à faire des projets, on note toujours la même chose: certaines parties du logiciel sont réécrites encore et encore. Si un projet commence à parler à une source de données à la main (API, base de données, crawling, fichiers, etc), tôt ou tard, quelqu’un dans l’équipe va commencer à écrire une abstraction. Celle-ci va grossir, et finir par implémenter au bout de nombreux mois, pour l’accès à la base de données, un semi ORM dégueulasse, mal testé / documenté.

Mais, si les requêtes SQL à la main c’est si bien, alors pourquoi ça arrive ?

Simplement parce que tout projet qui grandit a besoin d’une forme de gestion de la complexité. Ca passe par avoir un point, et un seul où sont décrites à quoi ressemblent les données, leurs accès, leurs garanties, leurs contraintes, et leurs validations. Ca passe aussi par permettre aux autres parties du projet d’utiliser automatiquement ces informations pour leur code métier, ainsi que la logique associée.

L’exemple de Django

L’ORM Django n’est pas vraiment le projet Python le plus propre. Il a plein de limitations et bizarreries, et son principal concurrent, SQLAlchemy est probablement une des meilleures libs au monde dans cette spécialité.

Mais !

Il est au coeur de ce qui a fait autant le succès colossal de Django.

Parce que Django est organisé autour de son ORM, il peut proposer:

A cela se rajoute le fait que tous les projets Django se ressemblent. Il est très facile de passer d’une équipe à une autre, d’un projet à une autre. La formalisation du schéma devient une documentation, et la seule source de la vérité à propos des données, et pas juste celle de la base. Et qui est commité dans Git. Pour savoir ce que fait un projet Django, il suffit de regarder urls.py, settings.py et models.py, et c’est parti.

Mais ce n’est pas tout. L’ORM ne fait pas que définir un point central inspectable et des outils réutilisables. Il donne aussi une base commune sur laquelle tout le monde peut compter.

Et pour cette raison, l’écosystème Django est très, très riche en modules tierces partis qui se pluggent en 3 coups de cuillère à pot:

La cerise sur le gâteau ? Parce que tout ça utilise l’ORM, tout ça est compatible ensemble. Votre authentification social auth va produire un objet user qui pourra se logger, consulter un dashboard qui contiendra le résultat d’un sondage sur un produit de la boutique.

Et ça marche avec MySQL, Oracle, SQLite ou PosgreSQL, comme l’intégralité du framework, gratos.

Ce n’est pas l’apanage de Django hein, RoR fait pareil.

Maintenant prenez un projet NodeJS. Pour le coup, pas parce que “JS ça pue” mais parce que la culture de l’ORM n’est pas très présente dans cette communauté. Non pas que ça n’existe pas, mais il n’y a pas de Django du monde du Javascript. Même les gros framework type Meteor n’ont rien d’aussi intégré.

Vous allez devoir réapprendre toute la mécanique de gestion de données si vous changez de projet, car ça sera fait différemment à chaque fois. Vous allez devoir former des gens.

Et surtout vous allez devoir réécrire tout ça.

Oh bien sûr, vous aurez une bibliothèque pour chaque chose, mais elle sera écrite différemment. Vous n’aurez pas d’objet User sur qui compter. Votre moyen de traduire le texte ? Pas le même. Vous utilisez Oracle mais l’auteur PostgreSQL ? Pas de bol. Vous voulez générer quelque chose à partir de votre modèle de données ? Ah, pourquoi vous croyez que facebook a créé GraphQL ! Une petite migration ? Vous avez le choix de l’embarras. Bon, maintenant vous allez gérer les dates, et 4 de vos bibliothèques les sérialisent différemment et une utilise l’heure locale au lieu d’UTC.

Évidemment, on sait que votre équipe ne testera pas tout ça, et ne documentera pas tout ça. La suivante non plus.

Donc non, l’ORM, ce n’est pas parce que “mais heu SQL c’est dur”.

C’est parce que ça permet de créer un monde autour.

Objection !

On a des abstractions qui ne sont pas des ORM…

L’important est d’avoir un modèle central, pas un ORM. Mais les ORM font ça particulièrement bien.

Il existe des abstractions (ex: LINQ) qui font un excellent travail pour masquer la source de données. Mais elles ne sont pas un remplacement pour un modèle introspectable central listant nature et contraintes.

Une bonne lib propose les deux.

Par exemple SQLALchemy propose un ORM, mais en vérité l’API de base est fonctionnelle et composable. Pour cette raison, on peut utiliser toutes fonctionnalités avancées de sa base de données avec SQLAlchemy car on a cette alternative à l’ORM à tout moment, et qui est compatible avec l’ORM.

Mais les perfs !

D’abord, on optimise pour les humains. En chemin, on optimise pour la machine. Quand ton ORM arrête de scaler, c’est un BON problème à avoir. Ca veut dire que le projet a atteint un certain succès, et qu’on peut investir dans la séparation avec l’ORM.

De plus, aucune technologie n’est faite pour être utilisée partout, tout le temps, pour tout le projet.

Google a connu ses débuts de succès en Python, et avec sa taille, a réécrit une partie en Java, C puis Go. C’est logique, on ne commence pas avec un langage bas niveau directement, c’est trop lent à écrire. Mais on ne garde pas un langage haut niveau pour tout quand ça monte dans les tours. Enfin les tours… Là on parle de centrifugeuse cosmique hein.

Car gardez en tête que vous n’êtes PAS Google. L’immense majorité des projets deviennent viables, rentables, puis pleins de succès, sans jamais atteindre l’échelle qui amène aux limites de l’ORM.

Quant à l’idée que votre stagiaire peut écrire une boucle avec une requête à chaque step… Il peut tout aussi bien écrire une requête SQL sans se protéger correctement l’injection de code. C’est con un stagiaire, faut le surveiller. C’est le principe, sinon il aurait un CDI.

Mais les fonctionnalités !

Les ORM bien faits n’empêchent pas d’utiliser toutes les fonctionnalités de son système. Et tous permettent d’écrire du SQL à la main si besoin. C’est comme les blocks unsafe de rust: on empêche pas, on isole.

L’idée c’est de garder ça pour le moment où on en a vraiment besoin. SQL est à la base de données ce que l’assembleur est à la machine. On n’écrit pas tout en assembleur de nos jours, ça n’est pas utile.

Root of all evil, tout ça.

Mais on ne change pas de base de données souvent !

L’argument “l’orm supporte plusieurs bases” n’est pas destiné à la migration de données d’un projet existant d’une base de données à une autre.

Pas. Du. Tout.

C’est pas que ça arrive jamais. Ça m’est déjà arrivé 2, 3 de fois.

Mais ce n’est pas le cas courant. Le cas courant c’est la réutilisation du code d’un projet précédent dans un nouveau projet utilisant une base de données différente. C’est la création d’un écosystème de modules qui ne sont pas dépendants de la base de données.

Si vous faites une “app” Django, vous pouvez la publier et elle sera utile pour toutes les bases de données supportées. Et c’est pour ça qu’il y autant d’outils.

Mais on pourrait avoir un modèle central sans ORM !

Oui, mais toutes les formes ne se valent pas.

Par exemple, Doctrine permet d’écrire son modèle dans un fichier YAML, et Hibernate dans un fichier XML.

Au final on écrit son modèle dans un langage moins complet, moins expressif, moins facile à débugger et avec moins de tooling. On perd encore plus en faisant ça qu’en passant de SQL à un ORM, sans autant de gains.

En prime, on peut vouloir de la logique de validation très complexe ou des choses à faire hors validation (signals, génération dynamique de modèle, etc), et là, pouet.

Une alternative, ça serait de se servir d’une lib de pur modèle (ex: l’excellent marshmallow) et de tout dériver de là. Une approche intéressante qui pourrait satisfaire tous les camps, mais que je n’ai jamais vu poussée jusqu’au bout dans aucun framework. Si vous cherchez un projet pour vos week-end :)

Lib VS framework

C’est un peu le vieux débat du découplage VS intégration qu’on retrouve dans la critique des ORM (ou dans vi VS vscode, POO vs fonctionnel, ta femme vs ta mère…).

Et comme d’habitude on retrouve toujours les meilleurs programmeurs du côté de ceux qui veulent le plus de liberté (vive SQL!) parce qu’ils ignorent complètement les problématiques qui vont plus loin que leur fichier de code. Faire fleurir un écosystème, gérer une communauté, favoriser la productivité, facilité l’intégration des membres de ses équipes… Tout ça sont des problématiques moins funs que de faire la requête parfaite avec le tout nouveau champ hyperloglog de PostGres.

Difficile de convaincre des gens qui sont non seulement excellents, mais qui sauront, seuls, être très productifs sans ORM. Surtout quand les gros projets qui atteignent des centaines de millions d’utilisateurs par jour finissent toujours par se séparer de leurs abstractions initiales.

Mais voilà, il ne faut pas perdre de vue que 2 projets sur 3 en informatique, échouent. Quasiment jamais pour des raisons techniques. Généralement la cause est humaine. Et l’ORM est là pour soutenir l’humain en créant un pivot sur lequel il peut compter, quitte à investir plus tard pour s’en passer.

C’est un excellent outil et une très belle réussite de l’informatique moderne. Si on sait l’aborder sans dogmatisme.


Monter son master node Interzone (ITZ) 21

Thu, 21 Dec 2017 12:52:47 +0000 - (source)

Acheter et vendre des cryptomonnaies, tout le monde comprend le principe. J’achète un truc, j’attends, je le revends. Si le prix a monté, je gagne du pognon, s’il est descendu, j’en perds.

Mais il existe d’autres formes d’investissement. L’une d’elles est le master node, et on va apprendre à en monter un.

Attention, ça demande de savoir administrer un serveur linux. Le tutoriel n’explique pas bash, apt, ssh ou make et suppose que vous êtes à l’aise avec ces outils. Si ce n’est pas la cas… pas de bol.

C’est quoi déjà ?

Bitcoin n’est plus la seule crypto, il y en a maintenant des centaines, et certaines essayent de se distinguer technologiquement.

Quelques-unes ont le concept de master node, des machines dont les propriétaires ont acheté un certains nombre de coins, et les bloquent. En échange de quoi, le réseau donne à la machine le droit de faire certaines opérations en plus, opérations qui rendent service au réseau (rendre les exchanges anonymes, accélérer les transactions, etc).

Le but des master nodes est double:

Mais pour le propriétaire du master node, qu’est-ce qu’on y gagne ?

Et bien tous les jours, une cagnotte est répartie équitablement entre tous les masternodes en récompense de leur service.

Par exemple, j’ai monté la semaine dernière un master node Vivo. Il a couté a l’achat 3200 euros. Tous les jours, il génère 5 vivos, soit actuellement 20 euros. En plus de cela, le vivo a augmenté de prix depuis l’achat, et le master node vaut maintenant 5000 euros si je décide de revendre les coins qui sont bloquées. Mais ça voudrait dire fermer mon master node.

Choisir un master node

Tous les master nodes ont des coûts et une rentabilité différents. Il existe des listing qui permettent de se faire une idée de la question.

Aujourd’hui je vais vous faire un tuto sur le master node Interzone, car il est très peu cher à l’achat: moins de 300 euros. Évidemment, il ne rapporte que 2 euros par jour.

Mais si vous voulez apprendre à faire des master nodes, c’est plus simple que de débloquer 5000 balles pour du vivo. Ou pire, un million pour un node dash :)

Interzone est aussi un wallet très basique, et donc simple à monter.

Acheter les coins

D’abord, il faut acheter les coins. Le nombre de coins à acheter est différent pour chaque type de node. Pour Interzone, c’est 5000 à bloquer, et donc on va en acheter 5001 pour les frais de transaction.

Pour se faire, il faut d’abord acheter une monnaie plus populaire sur une plateforme d’achat. Par exemple, vous pouvez acheter du Bitcoin sur Bitstamp.

Ensuite, faite un virement (withdrawal) vers une plateforme d’échange.

Tout ça se fait en plusieurs étapes.

D’abord, ouvrir des comptes sur ces sites, et faire les putains de vérifications de sécurité. Ouai, si vous vouliez trader anonymement c’est raté. Et parfois c’est long. Très long. Par exemple pour les achats par carte bancaire, Bitstamp m’avait demandé une photo de moi tenant mon passeport. Je sais pas si ils le font toujours, mais c’est over chiant.

Je vous laisse vous occupez de tous ça, on se revoit dans quelques jours.

Ça y est, de retour ?

Ok.

D’une part on va aller sur un exchange voir le cours de l’Interzone pour savoir combien de BTC acheter.

Par exemple à cet instant je vois que je peux acheter un ITZ pour entre 0.000002699 et 0.000007300 BTC. En regardant les cours, je vois que j’ai suffisamment de vendeurs pour avoir 5001 ITZ à 0.000002990 BTC, donc 0.0149529900 BTC. On rajoute les fees de l’exchange (0.0000373824750 BTC) et on prend en compte les fees de transactions de la block chain bitcoin (0.00265914 BTC).

Total: 0.017649512 BTC, soit 281.57 euros sur Bitstamp.

Parfois c’est plus, parfois c’est moins. Ça fluctue beaucoup.

On achète ça. Ça prend encore du temps…

Quand on a tout, on fait le virement sur son autre wallet, celui sur l’exchange.

Depuis l’exchange, on achète enfin son Interzone.

Monter son master node

En théorie on peut faire ça avec un client, mais en pratique ils ne marchent jamais. En plus il faut que ça reste allumé tout le temps et il faut avoir une IP statique.

Donc on va le faire sur son propre serveur linux.

Le plus pratique est d’avoir un serveur à soi qui tourne quelque part, ou de louer un VPS au prix minimal. En effet un master node, ça consomme que dalle en ressource et vous pouvez le mettre sur un raspberry pi si ça vous chante.

Parce que je suis pas maso, je vais expliquer que pour une distro, car il va falloir compiler. On va donc faire ça pour une Ubuntu 16.04.

D’abord on s’assure d’être toujours à l’heure:

sudo apt-get install ntp ntpdate
sudo update-rc.d ntp enable

On installe la tetrachiée de dépendances:

sudo apt-get install build-essential software-properties-common nano libboost-all-dev libzmq3-dev libminiupnpc-dev libssl-dev libevent-dev   libtool autotools-dev automake pkg-config libssl-dev libevent-dev bsdmainutils libboost-all-dev

Ensuite, on a besoin de downgrader une lib, donc, ppa:

sudo add-apt-repository ppa:bitcoin/bitcoin
sudo apt-get update
sudo apt-get install libdb4.8-dev libdb4.8++-dev

On download les sources:

wget https://github.com/projectinterzone/ITZ/archive/master.zip
unzip master.zip

Et on compile:

cd ITZ-master 
./autogen.sh
./configure
make

Vous pouvez aller boire un coup, pisser, faire la sieste. Ça prend pas mal de RAM, donc si vous serveur en a pas assez, faites les sur un ordi local avec la même version de linux.

Une fois tout ça terminé, le résultat est que dans le dossier ITZ-master/src vous avez deux executable:

C’est ce qu’on va utiliser. Si vous avez compilé en local, uploadez les n’importe où sur votre serveur.

Maintenant, on se place sur le serveur, dans le dossier qui contient interzoned et interzone-cli, et on va créer un fichier de config.

Pour le fichier de config, il vous faut l’adresse IP du serveur. On peut la choper avec:

$ dig TXT +short o-o.myaddr.l.google.com @ns1.google.com | awk -F'"' '{ print $2}'
xxx.xxx.xxx.xxx

Le port, lui, est toujours 55675 (soyez sur de pas le bloquer).

Puis on choisit un mot de passe pour le client:

$ tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1
BjHQ2T95Og2VNxsVDQ5qcFBU1eVNHP

On ouvre le fichier:

mkdir .interzone
vi .interzone/interzone.conf

Et on écrit dedans:

bind=:55675
rpcuser=interzone
rpcpassword=BjHQ2T95Og2VNxsVDQ5qcFBU1eVNHP

Puis on lance le daemon:

./interzoned -daemon -reindex

Si vous êtes consciencieux, ajoutez un service qui le démarre quand votre machine boot.

Au premier lancement, il va télécharger toute la block chain, mais ici c’est seulement 140Mo car interzone est tout jeune encore. Donnez-lui 30 minutes pour être tranquille.

Au bout de quelques minutes, votre client pourra déjà se connecter au master node et voir le progrès avec:

$ ./interzone-cli getinfo 
{
...
    "blocks" : 92477,
...
}

Il est temps de loader votre master node avec du pognon ! On se fait une adresse:

./interzoned getnewaddress MN1
1MKDs1yNze7VEr4zWHTKbiV3T3fBTwtxZ4

Et là vous allez sur votre exchange où vous avez acheté vos coins, et vous faite un transfert d’EXACTEMENT 5000 coins sur cette adresse (la votre, pas 1MKDs1yNze7VEr4zWHTKbiV3T3fBTwtxZ4, bande de moules). Pas un sou de moins. Pas un sou de plus.

Vous attendez que les coins arrivent. Ça peut prendre quelques minutes. Vérifiez avec:

./interzone-cli getbalance
5000.00

Si ça dit 5000, c’est bon.

Il est temps de démarrer notre master node.

D’abord, on arrête le daemon:

./interzone-cli stop

Ensuite, on se génère une clé privée pour notre master node:

./interzone-cli masternode genkey
EHLKJHoYfNaeziVMAtLs5678G9Em861r3456xtYh1TEotpY1

Ensuite on rouvre notre fichier de config, et on le met à jour pour contenir ça:

bind=:55675
rpcuser=interzone
rpcpassword=BjHQ2T95Og2VNxsVDQ5qcFBU1eVNHP
masternode=1
masternodeprivkey=EHLKJHoYfNaeziVMAtLs5678G9Em861r3456xtYh1TEotpY1

Ne soyez pas con, n’utilisez pas la clé que je poste dans ce tuto.

On va aussi chiffrer son waller, maintenant qu’on a des sous dessus, pour éviter de se le faire tirer. On génère un nouveau mot de passe, et on encrypt:

$ tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1
efFMgBGFpVhlQ0j8o8N4TaC5XdASyG
$ ./interzone-cli encryptwallet "efFMgBGFpVhlQ0j8o8N4TaC5XdASyG"

Faites un backup de votre wallet avant et du mot de passe. Si vous perdez ça, vous êtes niqué de chez niqué.

Vous pouvez déverrouiller le wallet avec:

./interzone-cli walletpassphrase efFMgBGFpVhlQ0j8o8N4TaC5XdASyG 120

efFMgBGFpVhlQ0j8o8N4TaC5XdASyG étant à remplacer avec votre mot de passe fraichement généré et 120 est le nombre de secondes pour le garder débloqué.

Il est temps de relancer le daemon:

./interzoned -daemon

Et finalement, le passer en master node:

./interzone-cli masternode start

C’est good.

Vous pouvez vérifier que ça a été accepté par le réseau avec:

./interzone-cli masternodelist | grep PUBLIC_IP

Toutes les 24h, vous pourrez joyeusement contempler le chiffre fourni par ./interzone-cli getbalance qui augmente. Et vous pouvez envoyer vos coins où vous le souhaitez (par exemple pour les revendre) avec un joli:

./interzone-cli walletpassphrase <votre password> 120
./interzone-cli sendtoaddress

Sécurité

Votre master node est ici configuré en mode “hot wallet”, c’est-à-dire que vos coins sont sur le serveur. Il existe une méthode, plus complexe, plus longue, mais plus sure, qui permet de le configurer en “cold wallet”. Vos coins sont alors sur votre machine, et un lien est fait enter votre machine et votre master node.

Si la sécurité vous importe, je vous invite donc à étudier la question.

Renforcer la configuration du firewall aussi est une bonne idée, puisque maintenant vous êtes une cible d’attaque potentielle.

Enfin, n’oubliez pas de vider tout l’historique bash en partant, histoire de pas avoir tous les trucs en clair dedans:

cat /dev/null > ~/.bash_history && history -c && exit

Et backupez comme des porcs aussi.

Profit ?

J’espère pour vous que tout ça va fructifier (et pour moi aussi). Si c’est le cas, vous savez ce qu’il vous reste à faire: envoyez-nous une carte postale de votre ile deserte. Ou des ITZ à 1AChuSLWAHQgmkGa84AHqBRCs1Z1cxHjnU. Ça marche aussi.

Si vous avez tout perdu, sachez que c’est la faute de Max. Je peux vous donner son adresse.

Et pour tous ceux qui n’ont pas la foi de faire tout ça, je pense que je vais monter un service qui permet de cliquer et acheter des masternodes automatiquement.


Powered by VroumVroumBlog 0.1.31 - RSS Feed
Download config articles