PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Qu’est-ce qu’une coroutine en Python, et à quoi ça sert ?

dimanche 14 décembre 2014 à 00:37

Si vous avez aimé les générateurs, vous avez du creuser un peu yield et vous apercevoir qu’on pouvait créer des coroutines avec. Mais sans vraiment comprendre ce que ça faisait.

On va se faire une petit intro. C’est un sujet vraiment avancé, donc si vous avez autre chose de moins compliqué à comprendre en Python (n’importe quoi à part les métaclasses :)), ne vous prenez pas la tête sur cet article. Ecoutez juste la musique :

D’abord, rappel sur le fonctionnement des générateurs (qui sont un prérequis de l’article, donc si besoin, relisez le tuto dédié) :

def soleil():
    print('Premier next()')
    print('Yield 1')
    yield 1
 
    print('Deuxième next()')
    print('Yield 2')
    yield 2
 
    print('Troisième next()')
    print('Yield 3')
    yield 3
 
    # pas de quatrième next(),
    # donc on ne passe jamais ici
    print('Pas vu')
 
# rappel, ceci ne déclenche pas le code 
# de soleil() puisqu'il y a yield dedans
print("Creation du generateur")
undeuxtrois = soleil()
 
# On execute le code jusqu'au yield 1
res = next(undeuxtrois)
print('res = %s' % res)
 
# On execute le code jusqu'au yield 2
res = next(undeuxtrois)
print('res = %s' % res)
 
# On execute le code jusqu'au yield 3
res = next(undeuxtrois)
print('res = %s' % res)
print('Good bye')
 
## Premier next()
## Yield 1
## res = 1
## Deuxième next()
## Yield 2
## res = 2
## Troisième next()
## Yield 3
## res = 3
## Good bye

Chaque fois qu’on appelle next() sur le générateur, il va exécuter le code jusqu’au prochain yield, et retourner la valeur de celui-ci, puis mettre le générateur en pause.

On peut assigner le résultat d’un yield, mais si on fait des next(), on obtient toujours None :

def lune():
 
    print('Premier next()')
    print('Yield 1')
    x = (yield 1)
 
    print('Deuxième next()')
    print('Avant le yield 2, x = %s' % x)
    print('Yield 2')
    x = (yield 2)
 
    print('Troisième next()')
    print('Avant le yield 3, x = %s' % x)
    print('Yield 3')
    x = (yield 3)
 
    print('Pas vu')
 
 
print("Creation du generateur")
generateur = lune()
 
res = next(generateur)
print('res = %s' % res)
 
res = next(generateur)
print('res = %s' % res)
 
res = next(generateur)
print('res = %s' % res)
print('Good bye')
 
## Creation du generateur
## Premier next()
## Yield 1
## res = 1
## Deuxième next()
## Avant le yield 2, x = None
## Yield 2
## res = 2
## Troisième next()
## Avant le yield 3, x = None
## Yield 3
## res = 3
## Good bye

La raison est que cette valeur doit venir de l’extérieur. Pour la fournir, il faut utiliser la méthode send() et non la fonction next().

Mais elle ne fonctionne pas du tout pareil. En fait, si on l’appelle cash pistache, ça plante :

print("Creation du generateur")
generateur = lune()
res = generateur.send("A")
print('res = %s' % res)
 
## Creation du generateur
## Traceback (most recent call last):
## File "test.py", line 24, in 
##   res = generateur.send("A")
##     TypeError: can't send non-None value to a just-started generator

C’est parce que, contrairement à next() qui va jusqu’au prochain yield, send() PART du dernier yield atteint pour aller au suivant.

Il faut donc d’abord arriver à un premier yield avant de faire un send(). On peut le faire en utilisant au moins un next().

Voici donc notre nouveau code :

def lune():
 
    print('On fait au moins un next()')
    print('Yield 1')
 
    x = (yield 1)
 
    print('Premier send(), x = %s' % x)
    print('Yield 2')
 
    x = (yield 2)
 
    print('Deuxième send(), x = %s' % x)
    print('Yield 3')
 
    x = (yield 3)
 
    # Comme on fait un next() et 3 send()
    # on arrive là
    print('Troisième send(), x = %s' % x)
    print('YOLOOOOO')
 
 
print("Creation du generateur")
generateur = lune()
 
next(generateur) # Ou generateur.send(None)
 
res = generateur.send("A")
print('res = %s' % res)
 
res = generateur.send("B")
print('res = %s' % res)
 
res = generateur.send("C")
print('res = %s' % res)
print('Good bye')
 
## Creation du generateur
## On fait au moins un next()
## Yield 1
## Premier send(), x = A
## Yield 2
## res = 2
## Deuxième send(), x = B
## Yield 3
## res = 3
## Troisième send(), x = C
## YOLOOOOO
## Traceback (most recent call last):
##   File "test.py", line 33, in 
##     res = generateur.send("C")
## StopIteration

send() agit donc comme next(). Il va aller jusqu’au prochain yield et lui faire retourner sa valeur. Mais il y a des différences :

La valeur peut être n’importe quel objet : string, int, classe, list, etc.

Bref, send() permet de créer un générateur donc le comportement n’est pas figé dans le marbre.

Par exemple :

def creer_fontaine():
    contenu = "soda" 
    while True:
        x = yield contenu
        if x:
            contenu = x
 
 
fontaine = creer_fontaine()
 
for x in range(5):
    print(next(fontaine))
 
# on change le contenu de la fontaine
fontaine.send("lait")
 
for x in range(5):
    print(next(fontaine))
 
soda
soda
soda
soda
soda
lait
lait
lait
lait
lait

On peut même s’en servir pour faire des trucs chelou comme injecter une dépendance à la volée ou contrôler le flux de son générateur :

def fuckitjaiplusdenomcool(start, inc=lambda x: x + 1):
    x = start
    # on controle le flux du générateur en changeant
    # la valeur de x qui peut tout stopper
    while x:
        sent = yield x
        if sent:
            inc = sent
        # la valeur de x dépend de ce bout de code
        # qui est injectable
        x = inc(x)
 
 
generateur = fuckitjaiplusdenomcool(1)
 
for x in generateur:
    print(x)
    if x > 10:
        # si on dépasse 10, on décrémente
        generateur.send(lambda x: x - 1)
 
## 1
## 2
## 3
## 4
## 5
## 6
## 7
## 8
## 9
## 10
## 11
## 9
## 8
## 7
## 6
## 5
## 4
## 3
## 2
## 1

Mais bon, pas la peine de rentrer dans des cas si compliqués.

Néanmoins, un cas d’usage de send() est de créer une coroutine. Une coroutine est simplement une tâche.

C’est un bout de code qui fait une tache, avec un bout d’initialisation, et un bout de finalisation, et un bout d’exécution.

Par exemple, j’ai un filtre qui prend un fichier rempli d’adresses IP. Il va recevoir du texte, et si le texte contient une adresse IP, il le signale, et remplit un compteur sur le disque.

Si on devait coder ça en objet on dirait :

import re
 
class Filtre:
 
    # initialisation
    def __init__(self, ipfile, counterfile):
 
        with open(ipfile, 'r') as f:
            self.banned_ips = set(f)
        with open(counterfile) as f:
            self.count = int(f.read())
        self.counterfile = open(counterfile, 'w')
 
    def check(self, line):
        # récupère les ip et check celles qui sont 
        # à filtrer
        ips = re.findall( r'[0-9]+(?:\.[0-9]+){3}', line)
        bad_ips = [ip for ip in ips if ip in self.banned_ips]
 
        # si il y a des ip à filtrer, on incrémente le compteur
        if bad_ips:
            self.count += len(bad_ips)
            self.counterfile.seek(0)
            self.counterfile.write(str(self.count))
 
        # on retourn les valeurs trouvées
        return bad_ips
 
    def close(self):
        self.counterfile.close()

On l’utiliserait comme ça :

f = Filtre("/chemin/vers/liste", "/chemin/vers/counteur")
for line in text:
    print(f.check(line))
f.close()

Notez que pour une tâche, l’API est toujours la même : initialiser, exécuter la tâche autant de fois que nécessaire, puis finaliser.

Les coroutines sont un mot qu’on met sur ce principe (initialiser, exec, finaliser), mais avec une API sous forme de générateur. Le même code en coroutine :

def filtre(ipfile, counterfile):
 
    # Initialisation
    with open(ipfile, 'r') as f:
        banned_ips = set(f)
    with open(counterfile) as f:
        count = int(f.read())
    counterfile = open(counterfile, 'w')
 
    # Exécution
    bad_ips = []
    while True:
 
        try:
            # entree et sortie de notre send(), qui équivaut
            # aux params de "check()"
            line = yield bad_ips
 
        # GeneratorExit est levé is on fait generator.close()
        # On ne peut pas ignorer cette erreur, mais
        # on peut mettre du code de finalisation ici.
        # Bon en vrai faudrait faire un finally quelque part
        # mais c'est pour l'exemple bande de peer reviewers
        except GeneratorExit:
                self.counterfile.close()
 
        ips = re.findall( r'[0-9]+(?:\.[0-9]+){3}', line)
        bad_ips = [ip for ip in ips if ip in self.banned_ips]
 
        # si il y a des ip à filtrer, on incrémente le compteur
        if bad_ips:
            self.count += len(bad_ips)
            self.counterfile.seek(0)
            self.counterfile.write(str(self.count))

On l’utiliserait comme ça :

f = filtre("/chemin/vers/liste", "/chemin/vers/counteur")
next(f)
for line in text:
    print(f.send(line))
# ceci raise GeneratorExit
f.close()

Généralement on veut pas se faire chier à appeler next() à chaque fois, donc toutes les libs à base de coroutine ont ce genre de décorateur :

def coroutine(func):
    def wrapper(*arg, **kwargs):
        generator = func(*arg, **kwargs)
        next(generator)
        return generator
    return wrapper

Afin de pouvoir faire ça :

@coroutine
def filtre(ipfile, counterfile):
    ...

Ca a un double usage : ça appelle next() automatiquement, et ça signale que la fonction est destinée à être utilisée comme coroutine.

Mais voilà, c’est tout, une coroutine c’est juste ça : utiliser un générateur pour faire une tâche qui consiste à s’initialiser, faire un traitement plusieurs fois, et optionellement, se finaliser. On utilisera une coroutine pour ne pas reinventer la roue car c’est un problème bien défini, qui a une solution. D’autant qu’une coroutine bouffe moins de ressources qu’une classe.

Les usages avancés des coroutines impliquent de chaîner plusieurs coroutines, comme des tuyaux.

Souvenez-vous, en Python il est courant de chaîner des générateurs :

def mettre_au_carre(iterable):
    for x in iterable:
        yield x * x
 
def filtrer_les_pairs(iterable):
    for x in iterable:
        if x % 2 == 0:
            yield x
 
def strigifier(iterable):
    for x in iterable:
        yield str(x)
 
# on pipe les données d'un générateur à l'autre
nombres = range(10)
carres = mettre_au_carre(nombres)
carres_pairs = filtrer_les_pairs(carres)
fete_du_string = strigifier(carres_pairs)
 
for x in fete_du_string:
    print(repr(x))
 
## '0'
## '4'
## '16'
## '36'
## '64'

On peut faire pareil avec les coroutines. Cependant, la logique est inversée : au lieu de lire les données, on les envoie :

@coroutine
def mettre_au_carre(ouput):
    while True:
        x = (yield)
        ouput.send(x * x)
 
@coroutine
def filtrer_les_paires(ouput):
    while True:
        x = (yield)
        if x % 2 == 0:
            ouput.send(x)
 
@coroutine
def strigifier(ouput):
    while True:
        x = (yield)
        ouput.send(str(x))
 
@coroutine
def afficher():
    while True:
        x = (yield)
        print(x)
 
nombres = range(10)
 
# chaque coroutine est la sortie d'une autre
afficheur = afficher()
fete_du_string = strigifier(afficheur)
paires = filtrer_les_paires(fete_du_string)
carre = mettre_au_carre(paires)
 
# on envoit les données vers la première coroutine
# et elle fait suivre aux autres
for x in nombres:
    carre.send(x)
 
## '0'
## '4'
## '16'
## '36'
## '64'

Vous allez me dire : “ça fait la même chose, et c’est plus compliqué, quel interêt ?”.

En fait, ça ne fait pas exactement la même chose.

Dans le cas des générateurs ordinaires, on déclenche le traitement par la fin. On fait une boucle qui demande quelle est la prochaine donnée, et si il y en a une, on l’affiche. C’est pratique si on sait qu’on a des données sous la main car on demande (next() est appelée par la boucle for) la donnée suivante à chaque fois : c’est du PULL.

Mais que se passe-t-il si on n’a pas encore les données ? Si on traite des données qui arrivent par évenement ?

Par exemple, si on écrit un serveur HTTP qui doit réagir aux requêtes ?

Dans ce cas, on ne peut envoyer (send()) la donnée suivante dans notre pipeline de générateurs uniquement quand elle arrive, et les coroutines font exactement cela : c’est du PUSH.

En résumé :

Si vous êtes arrivé jusqu’ici, vous méritez un cookie.

Ca tombe bien, ce blog utilise des cookies, et la loi m’oblige à vous le notifier.

Introduction au currying   Recently updated !

vendredi 12 décembre 2014 à 20:37

Le currying (ou Curryfication pour les frencofans) est le nom donné à une technique de programmation qui consiste à créer une fonction à partir d’une autre fonction et d’une liste partielle de paramètres destinés à celle-ci. On retrouve massivement cette technique en programmation fonctionnelle puisqu’elle permet de créer une fonction pure à partir d’une autre fonction pure. C’est une forme de réutilisabilité de code.

La forme la plus simple de currying est de réécrire une fonction appelant l’autre. Par exemple, soit une fonction pour multiplier tous les éléments d’un itérable :

def multiply(iterable, number):
    """ Multiplie tous les éléments d'un itérable par un nombre.
 
        Exemple :
 
            >>> list(multiply([1, 2, 3], 2))
            [2, 4, 6]
    """
    return (x * number for x in iterable)

On peut ensuite créer une fonction qui multipliera par 2 tous les éléments d’un itérable :

def doubled(iterable):
    """ Multiplie tous les éléments d'un itérable par un 2.
 
        Exemple :
 
            >>> list(doubled([1, 2, 3]))
            [2, 4, 6]
    """
    return multiply(iterable, 2)

C’est une forme de currying. On créé une fonction qui fait ce que fait une autre fonction, mais avec des arguments par défaut.

Python possède une fonction pour faire ça automatiquement avec n’importe quelle fonction :

>>> from functools import partial 
>>> tripled = partial(multiply, number=3) # on curryfie ici
>>> list(tripled([1, 2, 3])) # nouvelle fonction avec un seul argument
[3, 6, 9]

Cela marche car, je vous le rappelle, les fonctions sont des objets en Python. On peut mettre une fonction (je ne parle pas de son résultat) dans une variable, passer une fonction en paramètre ou retourner une fonction dans une autre fonction. Les fonctions sont manipulables.

Il n’est pas rare d’utiliser les fonctions anonymes comme outils curryfication. En Python, on ferait ça avec une lambda :

>>> tripled = lambda x: multiple(x, 3) 
>>> list(tripled([1, 2, 3]))
[3, 6, 9]

Certains outils, comme Ramda en Javascript, vont plus loin, et exposent des fonctions qui se curryfient automatiquement.

Pour ce faire, il faut inverser l’ordre qu’on mettrait intuitivement aux arguments dans la déclaration d’une fonction :

# au lieu de multiply(iterable, number), on a :
def multiply(number, iterable=None):
    # Si on a pas d'itérable passé, on curryfie
    if iterable is None:
        return partial(multiply, number=number)
    return (x * number for x in iterable)

Ainsi :

>>> list(multiply(2, [1, 2, 3])) # pas de currying
[2, 4, 6]
>>> quintuple = multiply(5) # currying automatique
>>> list(quintuple([1, 2, 3]))
[5, 10, 15]

L’intérêt de ce style, c’est qu’on peut composer des traitements à partir de plusieurs sous traitements, presque déclarativement :

def remove(filter, iterable=None):
    """ Retire tous les éléments d'un itérable correspondant au filtre.
 
        Exemple :
 
            >>> list(remove(lambda x: x >= 4, [1, 2, 3, 4, 5]))
            [1, 2, 3]
    """
    if iterable is None:
        return partial(remove, filter)
 
    return (x for x in iterable if not filter(x))
 
>>> smalls = remove(lambda x: x >= 4)
>>> list(smalls(tripled([0, 1, 2, 3, 4]))) # le traitement est auto descriptif
[0, 3]

Néanmoins, il faut savoir que ce style n’est pas pythonique. En effet, en Python on préférera généralement utiliser des suites suite de générateurs. Soit par intention, soit via yield.

Notre exemple serait alors :

>>> tripled = (x * 3 for x in [0, 1, 2, 3, 4])
>>> smalls = (x for x in tripled if x <= 4)
>>> list(smalls)
[0, 3]

De plus, cette technique suppose qu’on ne profitera pas de certaines fonctionnalités, comme les paramètres par défaut des fonctions Python.

C’est toutefois une bonne chose à connaître. C’est occasionnellement utile en Python et peut produire des solutions très élégantes. C’est également une bonne chose à comprendre pour aborder d’autres langages plus fonctionnels qui les utilisent bien plus comme le Javascript, le Lisp, ou carrément le Haskell.

Des bouts de Python cachés   Recently updated !

jeudi 11 décembre 2014 à 10:23

J’aime bien fouiner pour trouver des petits trucs insolites dans mes techos. Cet excellent post sur SO est un bon début, mais on découvre toujours de nouvelles choses.

ChainMap

Vous vous souvenez de ChainMap ? C’est top, mais uniquement en Python 3.

Et bien il existe une version cachée en Python 2 :

from ConfigParser import _Chainmap as ChainMap

C’est pas aussi complet (il n’y a pas les méthodes new_child et parents), mais ça peut servir.

Si néanmoins, vous avez besoin de l’implémentation complète, il existe un backport installable avec pip :

pip install Py2ChainMap

str.startswith, str.endswith et un tuple

Pour beaucoup de situations, une regex en Python, c’est overkill. On a in pour vérifier qu’une chaîne en contient une autre. Et on a str.startswith() et str.endswith() pour vérifier qu’une chaîne commence ou finit par une autre. Avec des strip()/split()/lower(), on finit souvent par s’en sortir, et l’usage de toutes ces méthodes est en général plus performant que d’utiliser re.

Mais si vous avez plusieurs chaînes à vérifier ? Pas de problème, str.startswith() et str.endswith() acceptent aussi des tuples :

>>> "Je suis une chaîne qui a du caractère".startswith('Tu')
False
>>> "Je suis une chaîne qui a du caractère".startswith('Je')
True
>>> "Je suis une chaîne qui a du caractère".startswith(('Tu', 'Je'))
True
>>>

Les espaces entre une instance et son attribut sont ignorés

Bien que la deuxième ligne soit très moche, elle est parfaitement valide :

>>> "Yolo".upper()
'YOLO'
>>> "Yolo"           .upper()
'YOLO'

Or on peut intercepter l’accès aux attributs à la volée en Python grâce aux méthodes magiques.

Ça veut dire qu’on peut implémenter des “symboles” en Python.

class SymbolMaker():
 
    symbols = set()
 
    def __getattr__(self, value):
        symbols.add(value)
 
>>> make_symbol = SymbolMaker()
>>> make_symbol   .start
>>> make_symbol   .end
>>> SymbolMaker.symbols
{'end', 'start'}

Je n’ai pas dit qu’on devrait le faire, j’ai dit qu’on pouvait :) D’ailleurs, y en a qui l’utilisent pour implémenter GOTO en Python. The troll is strong with this “feature”.

Quelle est la différence entre “bloquer” et “en cours d’exécution” ?   Recently updated !

mardi 9 décembre 2014 à 17:57

On vous dit qu’il faut faire attention en utilisant des technologies non bloquantes, car si on bloque dans la boucle d’événement, on bloque tout le programme, et on perd l’intérêt de l’outil.

C’est vrai, mais que veut dire “bloquer” ?

Car si je fais :

for x in range(1000000):
    print(x)

Mon programme va tourner longtemps, et la boucle d’événement va bloquer, n’est-ce pas ?

En fait, “bloquer” est un abus de langage car il y a plusieurs raisons pour bloquer. Dans notre contexte, il faudrait dire “bloquer en attente d’une entrée ou d’une sortie”. D’où l’appellation “Aynschronous non blocking I/O” des technos types NodeJS, Twisted, Tornado, Gevent, etc.

En effet, il faut distinguer deux causes d’attente à votre programme :

Le premier cas est impossible à éviter. Tout au mieux pouvons-nous répartir la charge du programme sur plusieurs cœurs, processeurs voire machines. Le code devra toujours attendre qu’il se termine, mais ça ira plus vite.

Dans le contexte de la programmation non bloquante telle qu’on vous en a parlé, on est donc dans le deuxième cas.

Il ne s’agit alors pas de s’interdire de faire des boucles ou autre opération longue (ou plutôt, c’est un problème d’optimisation ordinaire qui n’a rien à voir avec le fait de bloquer), il s’agit de ne pas “attendre à ne rien faire” quand une opération extérieure est en cours.

C’est ce que font naturellement NodeJS, Twisted, Tornado, Gevent & Co. Quand on fait un échange HTTP, le bout de données part, puis le reste du code continue de tourner, passant à la tâche suivante, en attendant que le paquet traverse le réseau, atteigne l’autre machine, qui vous répond finalement. C’est ce temps, incompressible, sans contrôle de votre côté, durant lequel il ne faut pas bloquer. Le gain de perf est que votre programme ne se la touche pas pendant les temps d’attente, mais bien entendu que VOTRE, lui, code va prendre du temps et “bloquer” le processeur. Il faut bien qu’il s’exécute.

Ce qu’on entend donc par “il ne faut pas faire d’opération bloquante dans un code qui est déjà non bloquant” c’est “il ne faut pas utiliser un outil à l’API bloquante au milieu d’autres outils non bloquants”.

Par exemple, n’utilisez pas requests avec Twisted, car requests est codé pour attendre sans rien faire jusqu’à obtenir une réponse à chaque requête, bloquant Twisted. Utilisez plutôt treq. C’est pareil pour la lecture d’un fichier, une requête de base de données, etc. Et il existe des boucles d’événements ailleurs que sur le serveur : une page Web possède sa propre boucle (c’est pour cela que tout JS est asynchrone), un toolkit GUI comme QT ou GTK aussi (c’est pour ça qu’ils utilisent la programmation événementielle), etc.

Maintenant vous allez me dire : mais pourquoi bloquer alors ? Pourquoi ne pas toujours éviter de bloquer ?

Et bien parce que si on ne bloque pas, on ne peut pas écrire un programme ligne à ligne. On est obligé d’adopter un style de programmation asynchrone puisqu’on ne sait pas quand le résultat de certaines lignes va arriver. Ça veut dire des callbacks, ou des futures, ou des coroutines, ou du message passing… Bref, un truc plus compliqué. Or, on n’a pas forcément besoin de ce niveau de performance. En fait, la grande majorité des programmes n’ont pas besoin de ce niveau de performance. Donc, on bloque en attendant, non pas Godot, mais l’I/O, parce que c’est plus simple à écrire. Pour pas se faire chier.

Il y a bien des moyens de contourner ce problème : les threads, le multiprocessing, les coroutines, etc. Parfois même, on ignore le problème : bloquer quelques ms au milieu d’une boucle d’événements une fois par seconde n’est pas un drame. Une fois que j’ai fini le dossier sur les tests unitaires, je vous ferai un dossier sur la programmation non bloquante, avec aussi une esquisse de la parallélisation.

En attendant, ne stressez pas parce que votre code “bloque” parce qu’il travaille longtemps, assurez-vous juste que les APIs que vous utilisez ne bloquent pas pendant l’I/O, et vous êtes ok.

Et comment savoir ? Et bien si une donnée rentre ou sort de votre programme (ça ne fait pas partie du code source), c’est de l’I/O. Si votre code ressemble à ça :

res = faire_operation_sur_IO()
faire_un_truc_avec_le_res(res)

Alors votre outil est bloquant, puisque qu’il compte sur le fait que la deuxième ligne sera exécutée à coup sûr quand la première sera terminée. Un outil non bloquant exigera quelque chose pour gérer le retour du résultat plus tard: un callback, une promesse, un yield

0bin.net est de nouveau en ligne   Recently updated !

lundi 8 décembre 2014 à 22:02

0bin était down, on a supprimé la page en cause, et on l’a remis up.

On ne sait pas trop comment lutter contre ça. Même TPB a dû avoir 40 noms de domaine pour s’en sortir, et franchement on n’a pas envie de se taper autant de boulot pour un projet qui ne nous rapporte rien.

Que faire donc ?

D’abord, créer une admin pour 0bin pour supprimer une page plus facilement. Ça nous évitera de dépasser les délais la prochaine fois, car je n’étais pas dispo et Max ne savait pas comment faire. C’est pas que c’est compliqué, mais ça prend du temps, faut lire la doc, se connecter au serveur, etc. Et forcément, c’est du temps qu’on préfère passer sur Dota ou des projets qui rapportent des sous.

Ensuite, prendre le temps de vous rappeler que 0bin.net n’est qu’une instance de 0bin. Un exemple. Multiplier les instances est encore le meilleur moyen d’avoir l’outil à disposition. Et le process est plutôt bien documenté.

Mais une fois qu’on a posté son truc sur une instance, si elle tombe, que fait-on ?

Pour le moment on est baisé.

On pourrait donc imaginer de rajouter une fonction de distribution à 0bin. L’idée serait de se lier via une API simple à d’autres 0bin en qui on a confiance. Si quelqu’un poste sur l’un, tous les autres reçoivent une copie. Ainsi dans le lot, il y en aura bien un qui résistera.

Je me tâte à faire ça dans la semaine. Déjà 0bin a besoin d’un portage qui supporte Python 3. Ensuite quelques tests unitaires ne feraient pas de mal. Une fois que c’est fait, je freeze cette version, et je lance 1bin.net, la version suivante. Avec du crossbar.io, du angularjs et du mode distribué. Le truc sera plus lourd, uniquement compatible 2.7 et plus compliqué, donc je préfère en faire une version à part.

Ça se tente, voir si j’arrive à débloquer un peu de temps pour le faire, mais c’est pas impossible. Stay tuned.

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?84 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}