PROJET AUTOBLOG


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

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

⇐ retour index

Look it up 7

mardi 8 mars 2016 à 09:58

Je travaille sur un projet magique. Chaque jour est une nouvelle découverte, une aventure !

Par exemple, c’est un projet utilisant l’excellent Django Rest Framework, une app Django très puissante qui permet de créer des API succulentes.

DRF est très flexible, et permet de régler tout un tas de paramètres à l’aide de classes de configuration. Par exemple, il extrait automatiquement tout champ lookup_field sur ses classes Serializer afin de choisir sur quel champ filtrer les données.

L’auteur du code que j’ai sous les yeux, je crois, a voulu être vraiment, mais alors vraiment sûr d’avoir un look up:

class FooViewSet(ModelViewSet):
 
    class Meta:
        model = Foo
        lookup_field = 'pk'
        lookup_fields = ('pk', 'data_id')
        extra_lookup_fields = None

En soi, c’est très drôle.

Et je pourrais arrêter l’article ici.

Mais non.

En effet, y a pas un truc qui vous choque ?

Je veux dire, autre que la sainte trinité des lookup fields…

Allez, relisez l’article depuis le début, je vous laisse une chance.


J’ai dit que DRF extrayait un champ lookup_field sur les classes Serializer, et comme vous pouvez le constater, l’auteur ici hérite joyeusement de ModelViewSet, mais pas du tout de Serializer.

Oui, parce qu’on est en pleine exploration de Fistland (Au fond du fun !™), ces 3 champs ne sont en aucun cas exploités automatiquement par DRF… car sur les Viewset, lookup_field est utilisé pour générer des URLs, et mes prédécesseurs ont créé un router custo qui override ceci. Mais si on retire les champs, ça pète tout car il y a des bouts de leur code qui supposent l’existence de ce champ.

Néanmoins, ne soyons pas complètement négatif, certaines classes héritent bien de Serialiser, et définissent aussi lookup_field. D’ailleurs une part de mon job est de migrer tout ça. Car la petite touche humoristique finale, c’est que lookup_field est deprecated depuis 3 releases dans DRF \o/ Mais deprecated sur les Serializer uniquement hein, pas les Viewset. Enfin je dis ça…

Se faciliter la vie quand on utilise asyncio dans le shell 19

lundi 7 mars 2016 à 15:03

Dernièrement j’ai mis les fonctions suivantes dans mon script PYTHONSTARTUP

Toujours choper une event loop toute fraiche, et ouverte:

import asyncio
 
def aloop(*args, **kargs):
    """ Ensure there is an opened event loop available and return it"""
    loop = asyncio.get_event_loop()
    if loop.is_closed():
        policy = asyncio.get_event_loop_policy()
        loop = policy.new_event_loop(*args, **kargs)
        policy.set_event_loop(loop)
    return loop

Lancer une coroutine dans une event loop jusqu’à ce qu’elle se termine:

import inspect
 
def arun(coro):
    """ Run this in a event loop """
    loop = aloop()
    if not inspect.isawaitable(coro):
        if not inspect.iscoroutinefunction(coro):
            coro = asyncio.coroutine(coro)
        coro = coro()
    future = asyncio.ensure_future(coro)
    return loop.run_until_complete(future)

Et ça s’utilise comme ça:

async def foo():
    await asyncio.sleep(1)
    print('bar')
 
arun(foo)

Ça me fait gagner pas mal de temps.

J’utilise aussi cette commande magique dans iPython qui permet de balancer des await en plein milieu du shell. Je suis en train de voir pour ajouter un truc similaire à ptpython.

Je me suis fait aussi un petit script pour lancer vite fait n’importe quel script dans une boucle asyncio de manière transparente, mais j’ai pas le temps de poster ça aujourd’hui donc ça sera pour la prochaine fois.

Comment tu as appelé ton utilisateur ? 6

vendredi 4 mars 2016 à 11:14

Je travaillais pour un client dont l’infrastructure m’avait pris quelques jours à prendre en main. Avec l’aide de toute l’équipe et de la patience, victoire, j’avais l’env de prod sur ma machine, et je pouvais faire tourner le produit…

Previouly, on Sam and Max

Je me rends sur la page d’accueil, qui me redirige instantanément sur l’admin du site Django.

Ceci n’avait aucun sens. Je regarde les requêtes HTTP, j’ai une 302 qui sort de nulle part. Je suis authentifié, ma conf nginx est pas déconnante, et dans le routing défini du urls.py, le pattern pour l’admin est le classique http://domain.tld/admin.

Rien de fou.

Alors après m’être arraché les cheveux, je vais voir un techos et lui demande si à tout hasard il a une idée.

- Il me dit “tu as appelé ton user comment ?”

– Oh, j’ai fait un ./manage.py createsuperuser vite fait pour tester en local avec username:admin, password:admin.

– Ben c’est ça, on peut pas créer un user qui s’appelle “admin”.

– Ah ?

– Oui on a un url pattern pour la page du profile utilisateur qui fait:

http://domain.tld/<username>

Quand tu arrives sur /, on redirige dessus automatiquement.

Du coup si tu l’appelles admin, tu as plus accès à ton profile…

And now…

Aujourd’hui, en explorant la base de code, je tombe sur ça:

class RegistrationForm(forms.Form):
 
    _reserved_usernames = [
        'accounts',
        'about',
        'admin',
        'clients',
        'data',
        'forms',
        'maps',
        ... + 50
    ]

J’ai de quoi écrire un bouquin avec le contenu des fichiers que je vois, je sens que ça va me faire quelques articles :)

Les interpréteurs alternatifs de python 20

jeudi 3 mars 2016 à 09:17

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

Bonjour, tous.
Vu qu’un des derniers posts de Sam&Max parlait de vitesse et des implémentations alternatives de Python, je propose un article sur l’état de ces différentes implémentations justement, et leur rapport à la vitesse.

Je commence par un petit rappel, pour nos lecteurs les moins aguerris avec l’écosystème Python : Python est un langage et aussi un interpréteur de référence, dont le vrai nom est CPython, écrit en C, qui livre également la bibliothèque standard Python (les modules datetime, urllib, collections, etc). CPython a des performances typiques de langage de script comme PHP ou Ruby, c’est à dire pas terribles, et a une conception assez simple, qui utilise des solutions simples pour répondre aux problèmes de langages de script. Celles qui font le plus parler d’elles (en mal évidement) étant la gestion de la mémoire par comptage de références (references counting), considérée moins performant qu’un vrai ramasse-miette (garbage collector) et assez limitée, et un verrou global de l’interpréteur, dont vous avez déjà dû entendre parler, le très mal-aimé GIL (Global Interpreter Lock), qui empêche entre autre à CPython d’avoir de vrai threads, c’est à dire des portions de code s’exécutant réellement en même temps, concurremment, sur des cœurs processeur différents. Néanmoins, rappelons-le encore : ces solutions permettent d’avoir un interpréteur simple et clair, pour lequel écrire des modules d’extensions en C est simple, ce qui est considéré actuellement comme le plus important. De plus, CPython lui-même n’ayant pas des performances significatives, du vrai multithread n’est pas un besoin pertinent. Si vous avez besoin de faire des traitements lourds, 9 fois sur 10 une bibliothèque spécialisée en C s’en occupe et fait elle même le traitement lourd en C dans ses threads, tel qu’OpenCV ou NumPy. En général vous avez très rarement besoin de vrai thread Python, et donc le GIL est relativement un faux problème.
La communauté Python s’est adaptée à cet absence de vrai concurrence, et différentes solutions sont apparues pour contourner le problème, je pense par exemple à l’excellent module multiprocessing, qui vous permet d’instancier différents interpréteurs et de les faire communiquer entre eux de manière transparente comme si c’était des threads.
D’une manière générale, quand il sera question d’un interpréteur alternatif, quatre points intéresseront la communauté :

Je ne parlerai ici que des interpréteurs se voulant concurrents de CPython et remplissant la même niche. Les interpréteurs remplissant une tache un peu différente, et/ou nécessitant des modifications significatives de la manière d’écrire du code Python, ou n’ayant pas un niveau d’aboutissement significatif ne seront pas abordés, comme Cython ou Stackless Python. Voici une liste complète des implémentations de python.

Déjà, niveau implémentations alternatives, parlons des historiques Jython et IronPython ; ces deux projets sont des interpréteurs python tournant dans la VM existante d’un autre langage, respectivement la JVM Java d’Oracle (Sun, avant son rachat) et le CLR/.Net C# de Microsoft, apportant ainsi différents bénéfices, entre autre un vrai ramasse-miette et aussi des vrai threads qui carburent. Ils ont aussi en commun d’avoir été peu adoptés (très peu dans le cas d’IronPython) et qu’ils ne sont plus très vivants (très peu dans le cas d’IronPython, déjà lâché par Microsoft en 2010) : dernière release de Jython: mai 2015 ; IronPython: juin 2014. Ça fait mal. De plus, si à leur sortie ils donnaient par moment de meilleures performances que CPython, ce dernier s’est beaucoup amélioré et maintenant ils se valent au mieux. De plus, tous les deux sont très lourds, avec une empreinte mémoire beaucoup plus grosse et un temps de démarrage sans comparaison avec CPython. À déployer, c’est complètement la mort aussi, rien à voir avec apt-get install python/pypy. Très beaucoup la mort pour obtenir un IronPython qui tourne sous Mono sous Ubuntu. Ces projets sont bien partis pour disparaître s’ils ne changent pas. Et le support de python3 est inexistant pour tous les deux. Et vos modules codés en C, vous pouvez vous asseoir dessus.

Pour la petite histoire (anecdote à recaser en soirée pour briller), Jim Hugunin, le créateur de Jython, a commencé à écrire IronPython pour démontrer aux gens que la plateforme .Net était mauvaise et biens moins bonne que la JVM pour écrire une implémentation de langage. Jim a commencé à écrire une implémentation de Python en .Net, et à sa grande surprise a réussi à faire une implémentation très aboutie en peu de temps et a trouvé la plateforme .Net tellement géniale qu’il a quitté le projet Jython pour lancer IronPython.

Ensuite vient Pypy, l’interpréteur Python en python orienté vitesse. Pypy existe depuis une dizaine d’année et vise à être un drop-in remplacement de CPython, c’est à dire qu’ils suffit de l’installer et de changer les commandes “python truc” par “pypy truc” et hop ça marche. Ça fonctionne, à condition de ne pas utiliser de modules compilés en C. Si c’est néanmoins le cas, Pypy peut utiliser les modules en C de CPython moyennant recompilation, mais attention, les perfs sont moins bonne qu’avec CPython, dû au fait que l’API vient de CPython et impose à Pypy, un interfaçage/fonctionnement qui n’est pas le sien et ne supporte pas toute l’API. Pypy délivre actuellement des performances en moyenne neuf fois supérieures à CPython sur leur suite de tests, ce qui est assez bluffant. Après, à chacun de considérer si la suite de test de Pypy est représentative d’une utilisation réelle, ce qui est le problème existentielle de chaque implémentation Python alternative qui vous met sous le nez un graphe montrant qu’elle est X fois plus rapide que CPython sur sa suite de tests. Pypy utilise également un GIL, donc ne fourni pas de vrais threads, mais a dans un coin un projet de passer à un modèle de concurrence qui permettrait de s’affranchir du GIL, et dispose par contre d’un vrai ramasse-miette. Le support de Python 3 est minimum, en juin 2014 est sorti Pypy 2.4 supportant python version 3.2, l’équipe informant qu’il est néanmoins plus lent que Pypy visant python 2.7, autant vous dire qu’on est pas en avance.

Vous avez pu tomber sur des noms genre Unladden Swallow et Stackless Python : Unladen Swallow était un Pypy-like antérieur par des ingénieurs de Google qui voulait générer du JIT avec LLVM en promettant des performances 5 fois supérieurs à CPython. Le projet a échoué à tenir ses promesses et a fini par mourir doucement lors que Google a retiré son financement. Ce qui a pu être sauvé a été intégré dans Pypy (des améliorations au module Pickle). Stackless  est un interpréteur modifié qui intègre les coroutines de base et d’autres trucs parallèles/asynchrone, il n’est pas un drop-in remplacement et demande d’écrire du python qui ne tournera que sur et pour lui.

Ensuite est venu il y a un an le “Pypy” de Dropbox, Pyston. Tout le monde a tapé sur Dropbox pour avoir réinventé sa roue carrée avec Pyston au lieu de contribuer à Pypy, Dropbox a répondu qu’ils utilisent une approche différente de celle de Pypy, ils utilisent un method-at-a-time JIT au lieu d’un tracing JIT, méthode qui a donné de si bons résultats avec le moteur javascript de Google, V8, en s’appuyant sur la célèbre LLVM, la “machine virtuelle pour le bas-niveau”. LLVM étant par ailleurs soutenue par des grands noms comme Apple et Intel, si Dropbox ne merde pas, ça pourrait donner un truc intéressant. De plus, contrairement à Pypy qui a choisi de peu supportter l’API C de CPython pour les modules, Pyston vise une compatibilité absolue avec l’API C, c’est une de ses priorité fondamentale . Nous pouvons en déduire que Dropbox a beaucoup de modules d’extension en C auxquels ils tiennent beaucoup. Pypy utilise un ramasse miette et un GIL. Un point fait néanmoins tousser avec Pyston, c’est que les créateurs déclarent ne vouloir que viser Python 2.7 et ne pas envisager de supporter python3. On a du mal à croire que Guido van Rossum travaille pour cette boite. Toutes ces informations viennent de la FAQ de Pyston. Le blog de Pyston, intéressant, explique leurs choix technique et rend compte de leur avancée : http://blog.pyston.org/.

Pyston a fait plusieurs chose intelligemment, notamment au lieu de réécrire un interpréteur, ils ont forké CPython et branché leurs systèmes de JIT dedans, ce qui leur a permis de sortir en moins d’un an une release de Pyston plutôt viable qui fait tourner beaucoup de choses et passe beaucoup de tests. Leur interpréteur est déjà 25% plus rapide que CPython sur leur suite de test, face à un Pypy 50% plus performant. C’est un petit tour de force.

Un énorme écueil d’écrire un interpréteur alternatif à CPython étaient les modules en C de la bibliothèque standard ; une partie significative des modules de la bibliothèque standard étaient écrits en C, donc si vous faisiez un interpréteur alternatif, il fallait aussi vous recoder dans le langage de votre interpréteur tous les modules de la bibliothèque standard que vous ne pouviez utiliser, ce qui implique de faire 10.000 tests pour s’assurer qu’ils ont exactement le même comportement que ceux de la bibliothèque standard, ce qui est extrêmement fastidieux et pénible. Depuis Python 3.3 en septembre 2012 , CPython fourni un recode en Python de tous ses module en C qui passent exactement la même suite de test et qui sont donc “garantis” (modulo erreur humaine, c’est une vaste et dure tâche) de se comporter exactement comme leurs congénères en C. C’est une énorme charge de travail en moins pour les développeurs d’interpréteurs alternatifs. Également, depuis Python 3.1 , l’import de modules, qui auparavant était une machinerie interne un peu obscure de l’interpréteur CPython, et donc qu’il fallait recoder en croisant les doigts pour que ça reproduise exactement le comportement de CPython sous peine de tout voir exploser, a été recodé en Python et est livré avec CPython, donc fini de redevoir recoder un système d’import, vous pouvez juste le reprendre et l’utiliser, ouf.

De tous, Pypy est le seul qui s’approche actuellement vaguement d’un “concurrent” sérieux à CPython, talonné par Pyston pour un futur proche. Et encore, ce n’est pas encore le drop-in remplacement parfait. Et tous ont un support de Python3 inexistant, sauf Pypy qui en a un insuffisant :). Et tous, en tant que VM complète (donc lourde) ou interpréteur-optimiseur visant à optimiser le code (et donc analyser), ont des temps de démarrage et d’atteinte de l’efficacité sans commune mesure avec CPython. Ils ne sont intéressants qu’en cas de long-running (genre, vos sites Django). Et tous utilisent un ramasse-miette, ce qui donne un comportement différents sur les objets “finalisés”. Alors que CPython a un comportement déterministe et détruit immédiatement les objets qui ne sont plus utilisés, les ramasse-miettes reportent leur destruction à un moment où il sera plus opportun de les détruire, potentiellement très longtemps ou jamais au pire des cas. C’est pour ça que par hygiène, il faut toujours libérer explicitement les ressources des objets représentants des ressources tel que les files-like objets représentants fichiers ou socket en utilisant leur méthode “close()” ou le manager “with” même si ça fonctionne très bien sans sous CPython. Pour les objets représentant des ressources limitées comme les fichiers, les sockets ou n’importe quel lien vers une ressource limitée procurée par l’OS, ne pas le faire signifie se prendre rapidement un stop de l’OS. D’une manière générale, CPython applique des solutions simples et déterministes qui offrent pas mal de garanties sympathiques (gestion des ressources, atomicité, alignement mémoire, adresse des objets en mémoire fixe, etc) qui sont maintenant considérées comme acquise et immuables par beaucoup de programmes python, malheureusement gênantes pour faire un interpréteur optimisant vraiment agressivement (là j’ai pas de sources mais c’est une complainte qui revient régulièrement dans les discussions des autres interpréteurs, surtout Pypy).

Parmi les news relatives à la vitesse de Python que l’article de S&M ne mentionne pas, la nouvelle release de la JVM vient avec un meilleur découplage des éléments la composant, et du coup il devient facile de l’utiliser pour un autre langage. IBM a fait un proof-of-concept en faisant un interpréteur python, qu’ils vont open-sourcer (et faire pareil avec Ruby). Pas d’altruisme là-dedans, c’est pour montrer au monde que leur VM est bien fichue. Ça pourrait être le nouveau Jython : http://www.infoworld.com/article/3014128/open-source-tools/ibms-open-source-jvm-project-could-also-speed-ruby-python.html.

Aussi, Microsoft s’étant visiblement, dernièrement, un peu amouraché de Python, on peut s’attendre que leur coopération à Python aille un peu plus loin. Ou pas.

Après, la triste vérité est que les langages dynamiques sont très durs à optimiser, parce qu’ils sont justement dynamiques, surtout Python où tout est modifiable, et donc interpréteurs/VMs ne peuvent être sûrs de rien et ne peuvent pas supprimer pleins de tests/résolutions/recherches, et que Python, malgré toutes ses implémentations pleines de bonne volonté, ne sera jamais aussi rapide que le C, ni même que Java. La dernière fois que j’ai regardé (~3 ans), le benchmark game donnait python 20x plus lent que le C, contre 2x pour le Java. Ça fait réfléchir. Tous les efforts poussés à fond, on tapera peut être le 5x plus lent que le C :)

Actuellement, un core-contributeur de CPython (français, cocorico !) travaille sur différents sujets visant à améliorer significativement les performances de CPython. C’est la première fois qu’un chantier aussi vaste est lancé visant à sérieusement améliorer les performances de CPython. Le site sur le sujet : http://faster-cpython.readthedocs.org/

Ces différents projets acceptent les dons. Je vous encourage évidemment vivement à donner. La vitesse de Python est l’un dernier écueil qui pourraient faire choisir à un décideur technique pressé un autre langage plutôt que Python. Des meilleurs interpréteurs Python, c’est plus de tâches accomplies en Python, donc plus de postes pour les développeurs Python, donc plus de demande, et donc de meilleurs salaires. Quand vous donnez à des projets Python, vous vous versez de l’argent à vous-même dans le futur. De plus, ça aide à faire reculer les parts de marché de cette horreur qu’est PHP et de la concurrence de Ruby et Go, et ça, c’est toujours ça de pris :)
Même une petite somme est significative. Avec l’euro fort, vous avez un grand pouvoir. Et un grand pouvoir implique de grandes responsabilités.
Donner à Cpython : https://www.python.org/psf/donations/
Donner à Pypy : http://pypy.org/

Voila, vous savez tout sur l’histoire des différents interpréteurs Python. Avec ça, vous pourrez vous la pétez en soirée et niquer des tonnes de meufs. Ou pas.

Il n’y a pas de mauvais script… 11

mardi 1 mars 2016 à 12:22

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

… il n’y  a que des scripts en passe de devenir bons. Avis aux débutants, croyez-y, vous ne le resterez pas.

C’est l’histoire d’un des premiers “vrais scripts” que j’ai écrits. Je venais de finir Learning Python et Programming Python de Mark Lutz (à l’époque la seconde édition “couvre python 2 !”).

J’étais même pas un script kiddie, et je ne programmais que pour m’amuser. J’étais fan de Sinfest, un web comic gavé de gros blasphèmes qui tâchent.

Je me suis mis en tête de télécharger tout ces comics d’un coup. Voilà ce que ce script était, et ce qu’il est devenu.

Script originel avec formatage encore moins lisible, sur 0bin.

#~ Script to DL all sinfest webcomics
#~ one can edit the START and END parameters to DL a subset
STARTDAY = 17  # 17
STARTMONTH = 01  # 01
STARTYEAR = 2011  # 2000
ENDDAY = 31
ENDMONTH = 01
ENDYEAR = 2012
ROOTDIR = 'Sinfest'
ROOTURL = "http://www.sinfest.net/comikaze/comics/"
IMGEXT = ".gif"
#~ Code start - no edition from here unless you know what you're doing
#~ --------------------
from distutils.file_util import copy_file
from urllib import urlopen
import os.path
import os
#~ ROOTDIR=os.getcwd()+ROOTDIR
#~ Check if date is out of range
 
 
def checkdate(d, m, y, ed, em, ey, sd, sm, sy):  # end date is ed, em, ey
    if y &gt; ey or (y == ey and m &gt; em) or (y == ey and m == em and d &gt; ed):
        return 0  # date over date range
    elif y &lt; sy or (y == sy and m &lt; sm) or (y == sy and m == sm and d &lt; sd):
        return 0
    else:
        return 1  # date below end date and above start date
#~ make the file name teh sae format than sinfest server
 
 
def getfilename(y, m, d, IMGEXT):
    M =`m`
    D =`d`
    Y =`y`
    if len(M) &lt; 2:
        M = '0' + M
    if len(D) &lt; 2:
        D = '0' + D
    filename = Y + '-' + M + '-' + D + IMGEXT
    return filename
#~ run the loop
if not os.path.isdir(ROOTDIR):
    os.mkdir(ROOTDIR)
for y in range(STARTYEAR, ENDYEAR + 1):
    for m in range(1, 13):
        for d in range(1, 32):
            filename = getfilename(y, m, d, IMGEXT)
            if checkdate(d, m, y, ENDDAY, ENDMONTH, ENDYEAR,
                         STARTDAY, STARTMONTH, STARTYEAR):
                if os.path.isfile(ROOTDIR + '/' + filename):
                    print '[' + filename + '] already exists'
                else:
                    src = urlopen(ROOTURL + filename)
                    if src.info().gettype() == 'image/gif':
                        dst = open(ROOTDIR + '/' + filename, 'wb')
                        dst.write(src.read())
                        dst.close()
                        print '[' + filename + ']' + ' copied'
                    else:
                        print "MIMETYPE ERROR (probably ERROR 404) ignored"
                    src.close()

On remarquera l’arrogance du jeune codeur fier de lui dans certains commentaires. Ils étaient à mon intention, en vrai, et montraient surtout que je ne savais pas trop ce que je faisais.

En vrac, parmi les choses qui me font sourire aujourd’hui :

Bon, il y a prescription, et puis je suis pas mal autodidacte, c’était pas si mal : ça fonctionnait ! Ça m’a quand même servi, de temps en temps, jusque 2012, pour remettre à jour mes dossiers d’images.


Et puis je l’ai relu et j’ai décidé de le reprendre avec ce que je savais de nouveau :

et ça a donné ça :

# -*- coding: utf-8 -*-
#! /usr/bin/python
# Script to DL all sinfest webcomics
# one can edit the START and END parameters to DL a subset
#-------------------------------------------------------------------------------
# Name:        getSinfest
# Purpose:
#
# Author:      Atrament
#
# Created:     12/04/2013
# Copyright:   (c) Atrament 2013
# Licence:     
#-------------------------------------------------------------------------------
 
import datetime
from urllib.request import urlopen
import os.path
import os
from urllib.error import HTTPError
 
 
def main():
    debut=datetime.date(2000,1,17)
    curGif=debut
    if not os.path.isdir("Sinfest"):
        os.mkdir("Sinfest")
    while curGif&lt;=datetime.date.today():
        if not os.path.isfile("Sinfest/"+curGif.isoformat()+".gif"):
            try:
                src=urlopen("http://www.sinfest.net/comikaze/comics/"+curGif.isoformat()+".gif")
                dst=open("Sinfest/"+curGif.isoformat()+".gif",'wb')
                dst.write(src.read())
                dst.close()
                print("    "+curGif.isoformat()+".gif : fetched.")
                src.close()
            except HTTPError as e:
                print("on "+curGif.isoformat()+" error happened. So is life.")
        else:
            print(curGif.isoformat()+" : ok")
        curGif+=datetime.timedelta(days=1)
 
if __name__ == '__main__':
    main()

Et honnêtement ça s’améliore (un peu): un shebang (pas tout à fait en haut du fichier, mais j’apprendrai plus tard), bien que je code à l’époque uniquement sous windows avec pyscripter, qui me fournit les commentaires de début de fichier, qui ne servent à rien, mais à l’époque m’éclatent. Ben oui, à ce moment là, je suis à peine adulte, et avec les heures que j’ai passé sur IDLE à tâtonnner, c’est une victoire pour moi.


C’est un peu plus tard encore que j’apprends que les requêtes internet qui prennent des heures, c’est pas obligatoire, parce qu’on peut faire du multi thread, et continuer de tourner pendant qu’on attend qu’une autre tâche s’accomplisse. Voilà de quoi faire rêver un jeune programmeur : la puissance du multi-coeurs à la portée de mon code ! (non, je ne savais pas que c’est faux). En conséquence, mon script évolue, pour devenir… ça. Attention, ça pique les yeux.

# -*- coding: UTF-8 -*-
# ---------------------------------------------------------------------
# Author:      Atrament
# Licence:     CC-BY-SA https://creativecommons.org/licenses/by-sa/4.0/
# ---------------------------------------------------------------------
# All libs are part of standard distribution for python 3.4
import imghdr
import os
import queue
from threading import Thread
import datetime
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
import zipfile
import sys
 
 
# Useful functions
def make_cbz(directory):
    for year in range(2000, datetime.date.today().year + 1):
        with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive:
            for gif_file in [x for x in os.listdir(directory) if x.split("-")[0] == str(year)]:
                archive.write(directory + '/' + gif_file, arcname=gif_file)
        print("Sinfest-{}.cbz has been generated".format(year))
 
 
def confirm(prompt):
    if input(prompt + " (y/n)") in "yY":
        return True
    else:
        return False
 
 
def file_needs_download(filename):
    """Checks whether a file exists, is corrupt, so has to be downloaded again
    also cleans garbage if detected"""
    if not os.path.isfile(filename):
        # many comics are *supposed* to be missing,
        # no need to output for these (uncomment for debug)
        # print("IS NOT FILE :", filename)
        return True
    elif os.path.getsize(filename) == 0:
        print("WRONG SIZE for", filename)
        return True
    elif filename.split(".")[-1] != "gif":
        print(filename, "IS NOT GIF")
        return True
    elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None:
        # Encoding error...
        print("WRONG FILE STRUCTURE for", filename)
        return True
    else:
        return False
 
 
def conditional_download(filename, base_url):
    if file_needs_download(filename):
        try:
            src = urlopen(base_url + filename)
            dst = open(filename, 'wb')
            dst.write(src.read())
            # gracefully close theses accesses.
            dst.close()
            src.close()
            print("\t" + filename + " : fetched.")
        except HTTPError:
            # many days do not have a comic published.
            # no need to flood the console for this.
            pass
        except URLError:
            pass
            print("network error on " + filename)
        finally:
            # clean garbage on disk, useful if failure occurred.
            file_needs_download(filename)
 
 
class ThreadedWorker():
    def __init__(self, function=None, number_of_threads=8):
        self.queue = queue.Queue()
 
        def func():
            while True:
                item = self.queue.get()
                if function:
                    function(item)
                else:
                    print(item, "is being processed.")
                self.queue.task_done()
 
        self.function = func
 
        for i in range(number_of_threads):
            t = Thread(target=self.function, name="Thread-{:03}".format(i))
            t.daemon = True
            t.start()
 
    def put(self, object_to_queue):
        self.queue.put(object_to_queue)
 
    def join(self):
        self.queue.join()
 
    def feed(self, iterator):
        for task in iterator:
            self.queue.put(task)
 
 
def download_sinfest(target_folder):
    """
    Creates a directory and fetches Sinfest comics to populate it in full.
    """
    if not os.path.isdir(target_folder):
        os.makedirs(target_folder)
    os.chdir(target_folder)
 
    f = lambda filename: conditional_download(filename, "http://www.sinfest.net/btphp/comics/")
    # Make a worker with this function and run it
    t = ThreadedWorker(function=f, number_of_threads=20)
    # structure of comprehended list is a bit complex to generate all file names
    files = [(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat() + ".gif"
             for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)]
    t.feed(files)
    t.join()
 
 
if __name__ == "__main__":
    if any(("y", "Y", "-y", "-Y" in sys.argv)):
        folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/")
        print("\nproceeding to download...")
        download_sinfest(folder)
        print("\ngenerating comic book files (.cbz)...")
        os.chdir(folder)
        os.chdir("..")
        make_cbz(folder)
    else:
        folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/")
        while not confirm("Target to downloads is {} ?".format(folder)):
            folder = input("Please enter new folder (N to abort) :")
            if folder in "nN":
                exit(0)
        if confirm("Proceed to download ?"):
            download_sinfest(folder)
        if confirm("Do you want to generate cbz (comic books) files ?"):
            os.chdir(folder)
            os.chdir("..")
            make_cbz(folder)
        input("Finished. Please press Enter")

C’est une horreur. Si vous êtes de ces perfectionnistes qui lisent le code et font la code review par habitude, je suis désolé, vous avez du pleurer.  Le commentaire sur la lib standard “en intro” montre que à ce moment là, j’ai un peu conscience qu’on peut accéder à d’autres modules, mais on est loin de pip pour moi. Je tente maladroitement d’exploiter sys.argv (avec des erreurs qui pourraient être catastrophiques),  et l’input à défaut. Une fonctionnalité neuve est apparue : faire des archives comic book en zip. Je suis fier.

Mais le côté comique de ce code, c’est la classe ThreadedWorker. C’est ptêt bien ma première ‘vraie’ classe, mais j’implémente moi-même un dispatcheur de jobs sur des threads. Il y a de quoi être fier, mais on n’est pas du tout dans du python propre, là. C’est ballot, la version précédente était pas si mal, niveau clarté.

Et dire que la lib standard en fournit un, de dispacheur de jobs…


Je passe quelques itérations sur des détails, aujourd’hui il en est là, ce script.

#! /usr/bin/env python3.5
import imghdr
import os
import datetime
import zipfile
from concurrent.futures import ThreadPoolExecutor
 
import requests
import begin
 
 
# Useful functions
def make_cbz(dst_directory, src_directory):
    for year in range(2000, datetime.date.today().year + 1):
        with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive:
            for filename in os.listdir(src_directory):
                if filename.startswith(str(year)):
                    archive.write(src_directory + "/" + filename, arcname=filename)
        print("Sinfest-{}.cbz has been generated".format(year))
 
 
def file_needs_download(filename):
    """Checks whether a file exists, is corrupt, so has to be downloaded again
    also cleans garbage if detected"""
    if not os.path.isfile(filename):
        # many comics are *supposed* to be missing,
        # no need to output for these (uncomment for debug)
        # print("IS NOT FILE :", filename)
        return True
    elif os.path.getsize(filename) == 0:
        print("WRONG SIZE for", filename)
        return True
    elif filename.split(".")[-1] != "gif":
        print(filename, "IS NOT GIF")
        return True
    elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None:
        # Encoding error...
        print("WRONG FILE STRUCTURE for", filename)
        return True
    else:
        return False
 
 
def conditional_download(filename, base_url, caller=None):
    if file_needs_download(filename):
        src = requests.get(base_url + filename)
        # manage failure to download
        if src.status_code == 404:
            src.close()
            return  # ignore it, that file is simply missing.
        if src.status_code != 200:  # an error other than 404 occurred
            # print("Error {} on {}".format(src.status_code, filename))
            src.close()
            if caller:  # retry that file later
                caller.submit(conditional_download, filename, base_url, caller)
        # actually copy that file
        dst = open(filename, 'wb')
        dst.write(src.content)
        # gracefully close theses accesses.
        dst.close()
        src.close()
        print("\t{} : fetched.".format(filename))
 
 
def download_sinfest(target_folder):
    """
    Source function for the process
    Creates a directory and fetches Sinfest comics to populate it in full.
    """
    if not os.path.isdir(target_folder):
        os.makedirs(target_folder)
    os.chdir(target_folder)
 
    with ThreadPoolExecutor(max_workers=64) as executor:
        for file in ("".join([(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat(), ".gif"])
                     for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)):
            executor.submit(conditional_download, file, "http://www.sinfest.net/btphp/comics/", executor)
 
 
@begin.start
def run(path: "folder in which the comics must be downloaded" = os.path.expanduser("~/Sinfest/"),
        makecbz: "Compile CBZ comic book archives" = False):
    """Download the Sinfest WebComics"""
    download_sinfest(path)
    if makecbz:
        dst = path[:-1] if path.endswith('/') else path
        dst = "/".join(dst.split("/")[:-1])
        make_cbz(dst, path)
    print("Finished. Goodbye.")
    exit()

Enfin je m’autorise à utiliser des modules tierces. Alors, ça marche, même plutôt vite et bien. Mais c’est largement perfectible : si c’est mieux que par le passé, ça ne correspond pas à ce que je code aujourd’hui : il y a beaucoup de code hérité des anciennes versions et la prochaine évolution serait de le réécrire entièrement. Mais je ne lis plus vraiment Sinfest.


 

Mais à écrire ce texte, j’ai pris les nerfs, et j’ai refait tout ça. J’ai fait un effort, et j’ai tout jeté à github.

Encore un pas de mieux : les fichiers ont du sens, il y a une classe abstraite pour faire une pseudo-API, c’est devenu pratiquement évolutif.

 

La conclusion à l’attention du débutant : chacun voit et juge son code et celui des autres selon son niveau de compétence à l’instant T. On a tous été fiers comme Artaban de bouts de code laids à nos yeux d’aujourd’hui. Débutant qui m’a lu (merci, t’as bien du courage), garde tes scripts à travers les années, reprends les, tu te rendras compte de tous les progrès que tu fais au fil du temps. Quand un type te dis sur internet que ce que tu fais n’est pas “pythonique”, il a sûrement raison, de son point de vue, et il y a sans doute un autre gars qui lui dira la même chose demain. La courbe d’apprentissage est longue comme la vie.