Scripts python pour petites taches d'admin

Bonsoir,

J’ai commencé à regrouper les petits outils/scripts python que j’utilise pour mon instance Yunohost ici : fipaddict / t4la · GitLab

Pour l’instant, j’ai mis 2 outils mais d’autres sont dans le tuyau… :

  • ext_backups : qui permet d’externliser des backups YNH vers un serveur SFTP (sans SSH)
  • chk_updates : pour recevoir par mail les éventuelles mises à jour disponibles (résultat de yunohost tools update ...

Je suis un newbie avec python, YNH et tous ces outils en général, donc je suis bien évidemment preneur de toutes les remarques/suggestions. Après tout, c’est aussi et surtout pour cela que le repository est publique :slight_smile:

3 Likes

Hmmm en regardant vite fait, la première remarque qui me vient à l’esprit c’est que c’est un peu chelou d’avoir à la fois un .sh et un .py pour chaque script

Moi j’aurais tendance à mettre le shebang #!/usr/bin/env python au début de tes fichiers python, et à les appeler directement plutôt que le .sh … (en plus ton .sh contient des chemins absolu hardcodé, ça donne un peu la grimace)

  • Pour ton ext_backup.sh, je vois que tu veux lui passer un argument qui est le fichier de config … moi j’aurais tendance à utiliser par défaut le fichier de config .ext_backup.conf qui serait dans le même répertoire que le script lui-même (donc /home/t4la/opt/t4la/.ext_backup.conf dans ton cas - il suffit juste de pas le versionner si ça contient des infos persos). Et donc en python ce chemin serait, à l’aide de la variable magique __file__ :
conf_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), ".ext_backup.conf")
  • Pour ton chk_updates.py, tu veux utiliser le résultat de la commande yunohost tools update --quiet --output-as json. Dans de cas en python, il “suffit” de faire un truc du genre:
import subprocess
import json

raw_infos = subprocess.check_output(["yunohost", "tools", "update", "--quiet", "--output-as", "json"])
infos = json.loads(raw_infos)
  • Eeeeet en continuant à lire le script, je vois aussi:
try:
    # [ ... ]
except ValueError:
    pass

eeeeet … bon, des fois ça peut être vraiment légitime de faire un except: pass, mais la plupart du temps pour des gens qui débute, c’est un peu la méthode de l’autruche pour résoudre un bug … là ça ressemble furieusement à un cas mal géré par ton code

  • Je vois aussi:
str_check += '* ' + updates_type + ' * ' +\
                                 str(data['name']) + ' : ' + \
                                 str(data['current_version']) + ' -> ' + \
                                 str(data['new_version']) + ' \n'

ça marche sans doute, mais si tu es en python au moins 3.6 (je crois?) je t’encourage a utiliser les f-strings qui sont quand même vachement plus sympa. C’est des trucs du genre f"Je m'appelle {prenom}" où le {prenom} va automatiquement être remplacé par le contenu de la variable prenom. (Note le f avant les quotes de la string qui déclenche ce comportement)

Dans ton cas ça donne:

str_check += f"* {update_type} * {data['name']} : {data['current_version']} -> {data['new_version']}\n"
3 Likes

Je continue en regardant vite fait ext_backups.py:

  • Je vois:
    if len(sys.argv) != 2:
        print('Need configuration_file_name')
    else:
        # ... un truc qui s'étale sur 20~30 lignes

Dans ce genre de cas tu peux “économiser” facilement un cran d’indentation en quittant directement le programme (ou la fonction) car tu ne fera rien d’autre dans le cas où aucun argument n’est fourni. Un cran d’indentation c’est pas grand chose, mais quand tu es en a 2 ou 3 qui sont pas super utile, ça donne du code indenté de moulte crans pour pas grand chose … Bref, ça donne un truc comme :

    if len(sys.argv) != 2:
        print('Need configuration_file_name')
        sys.exit(1)
    
    # ... ici le truc qui s'étale sur 20~30 lignes 
    # (note l'indentation en moins, car si on continue 
    # c'est bien qu sys.argv == 2, sinon on aurait quitté le programme)
  • Ensuite:
        try:
            with open(sys.argv[1], 'r') as conf_file:
                config = yaml.safe_load(conf_file)
            
                # ... ici un traitement qui s'étale sur 20~30 lignes
        except ValueError:
            print(' YAML parser failed to read configuration file !')

Deux remarques ici :

→ Ton traitement après avoir chargé config est “indenté” dans le with … or, une fois que tu as lu le fichier yaml et stocké dans config, il n’y pas de raison de garder le fichier ouvert plus longtemps. (Meme si tu clos le fichier, la variable config et son contenu continue d’exister) Tu peux tout à fait dé-indenter ton code d’un cran pour économiser de l’indentation (ça n’a aucun impact sur les perfs ou quoi, c’est surtout cosmétique et pour ne pas donner l’impression à la personne qui te lit qu’il y a une nécessité à garder le fichier ouvert ou quoi …)

→ ton except ValueError affiche un message à propos du YAML qui aurait une erreur … mais en réalité, ton try/except englobe vraiment beaucoup de ligne, et il se pourrait tout à fait que ce soit une autre ligne de code qui déclenche une ValueError. En réalité c’est mieux de mettre un premier try/except uniquement autour des lignes concernées et de vraiment séparer les différentes opérations de ton code : 1) charger le fichier de config ; 2) créer les objets target et source ; 3) déclencher les actions ; 4) nettoyer les trucs … tout ça nécessite potentiellement une gestion d’erreur et donc des try/except indépendant (enfin, si c’est pertinent hein)

Bref, pour le morceau dont il est question ça donne:

        try:
            with open(sys.argv[1], 'r') as conf_file:
                config = yaml.safe_load(conf_file)
        except ValueError:
            print(' YAML parser failed to read configuration file !')
            sys.exit(1)

        # ... ici un traitement qui s'étale sur 20~30 lignes
3 Likes

Ah et pour finir, attention à utiliser trop de try/except à outrance … Les gens qui apprennent les try/except ont souvent l’impression que c’est bien d’en mettre partout / tout le temps. En réalité, tout le sujet de la gestion des erreurs est un sujet complexe et dépends de beaucoup de chose dans ce qu’on souhaite que le programme fasse. Des fois l’objectif peut être d’améliorer l’expérience utilisateur en cas de problème. D’autre fois l’objectif peut être d’assurer la résilience face à des problèmes techniques (par ex. soucis temporaire sur le réseau, …). D’autre fois on pense bien faire en faisant un try/except mais on empêche l’utilisateur de pouvoir debug facilement son problême … (toute ressemblance avec des situations du code de YunoHost serait fortuite :grimacing: )

Dans ton cas, lorsque le chargement du fichier YAML échoue tu affiches un message d’erreur « YAML parser failed to read configuration file ! » … mais en faisant ça, le message d’erreur initial n’est plus montré et on ne sais pas pourquoi le programme a “fail” … Alors que peut-être que le message de base de Python indiquait plus précisément où se trouve l’erreur.

Bref, un truc cool a faire est plutôt d’écrire (en utilisant une f-string par exemple):

except ValueError as e:
    print(f' YAML parser failed to read configuration file ! Error: {e}')
    sys.exit(1)

(note le as e qui permet de manipuler l’exception initiale)

4 Likes

Merci pour cette réponse pour le moins complète et détaillée. Cela va me permettre de progresser. Rien qu’à la première lecture, j’ai le sentiment d’apprendre des trucs. Je vais maintenant prendre le temps d’intégrer toutes ces remarques/suggestions et d’avancer pas à pas.
Encore un grand merci à toi.

1 Like