====== Invoke (pyinvoke) ======
[[http://www.pyinvoke.org/|Invoke]] est un **ordonnanceur** et un **exécuteur** de tâches, c'est çà dire que il permet de décrire un workflow d'étapes pour atteindre un objectif et l'outil exécute celles-ci dans le bon ordre.
Il repose sur la définition des tâches (//task//) via l'annotation //@task//.
Ces tâches sont définies au sein d'un fichier //tasks.py//.
//tasks.py//
from invoke import task
@task
def remove_all(ctx):
ctx.run('rm -rf /tmp/my_dirs/work')
$ invoke -l
Available tasks:
remove-all
$ invoke remove-all
$
===== Mise en oeuvre =====
Création de l'environnement virtuel de test
$ ~/Workdirs$ virtualenv -p python3 test_invoke
Running virtualenv with interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/mdexet/Workdirs/test_invoke/bin/python3
Also creating executable in /home/mdexet/Workdirs/test_invoke/bin/python
Installing setuptools, pip, wheel...done.
$ ~/Workdirs$ cd test_invoke/
$ ~/Workdirs/test_invoke$ source ./bin/activate
Installation de pyInvoke
$ ~/Workdirs/test_invoke$ pip install invoke
Collecting invoke
Downloading invoke-0.21.0-py3-none-any.whl (153kB)
100% |████████████████████████████████| 153kB 3.9MB/s
Installing collected packages: invoke
Successfully installed invoke-0.21.0
===== Créer une tâche =====
Pour créer une tâche il suffit
* de créer un fichier //tasks.py//
* de créer une fonction qui prend //au moins// un //contexte// ctx en argument
* et de l'annoter avec //@task//
@task
def do_something(ctx):
....
Une tâche peut exécuter une fonction python //ou// une commande système avec //ctx.run()//.
**Appel de fonction**
def do_it_right():
//....
@task
def do_something(ctx):
do_it_right()
**Appel de commande système**
@task(pre=[clean_workdir, get_last_commit, compile_project],
post=[deploy_artefact, update_website])
def build(ctx):
ctx.run('maven install')
===== Ordonnancement des tâches =====
Un tâche peut nécessiter plusieurs taches en amont et/ou en aval du processus.
Pour modéliser cet enchaînement, il faut indiquer à la tache quels sont
* ses prédécesseurs avec la propriété //pre//
* et ses successeurs avec la propriété //post//
@task(pre=[clean_workdir, get_last_commit, compile_project],
post=[deploy_artefact, update_website])
def build(ctx):
do_something()
La succession des tâches s'effectue de la première à la dernière **sauf** si
* une exception est levée
* une commande système ( //ctx.run()// ) retourne un code erreur.
**Exemple d'exception non gérée**
@task
def get_last_commit(ctx):
a = 10/0
print('Get last commit')
$ invoke build
Clean workdir
Get last commit
Traceback (most recent call last):
File "/home/user/Workdirs/test_invoke/bin/invoke", line 11, in
sys.exit(program.run())
...
File "/home/user/Workdirs/test_invoke/tasks.py", line 10, in get_last_commit
a = 10/0
ZeroDivisionError: division by zero
**Exemple d'exception gérée**
@task
def get_last_commit(ctx):
raise Exit('hostname not found')
print('Get last commit')
$ invoke build
Clean workdir
hostname not found
==== Tâche par défaut ====
Pour désigner une tâche par défaut, il suffit de lui donner la propriété //default=True//
@task(default=True)
def build(ctx):
...
==== Exemple complet d'ordonnancement ====
from invoke import task
@task
def clean_workdir(ctx):
print('Clean workdir')
@task
def get_last_commit(ctx):
print('Get last commit')
@task
def compile_project(ctx):
print('Commit')
@task
def deploy_artefact(ctx):
print('Deploy')
@task
def update_website(ctx):
print('Update website')
@task( pre=[clean_workdir, get_last_commit, compile_project],
post=[deploy_artefact, update_website])
def build(ctx):
print('Building')
$ invoke build
Clean workdir
Get last commit
Commit
Building
Deploy
Update website
===== Exemples =====
==== Convertir en PDF des fichiers asciidoc ====
Imaginons un projet avec
* un répertoire //main/doc// contenant la documentation en [[asciidoc]]
* un répertoire //build/doc// contenant la documentation finale
* un listing de fichiers dans //main/src// à mettre sous forme de document asciidoc
Décomposons le workflow.
Il faut
* générer un document //_listing.adoc// avec la liste des fichiers dans //main/src//
* générer un PDF dans //build/doc//
* avant il faut s'assurer que //build/doc// existe
from invoke import task
import os
import shutil
from mako.template import Template
work_dirname = '/tmp/project_doc'
def collectfilenames(path):
"""
Collect full qualified names of file within path.
:param path: directory to scan
:return: full qualified filenames from this path
"""
path_list = []
for dirpath, _, files_name in os.walk(path):
for name in files_name:
path_list.append("{}/{}".format(dirpath, name))
return path_list
def recreate_dir(dirname):
"""
Remove and recreate dirname
:param dirname:
"""
if os.path.exists(dirname):
shutil.rmtree(dirname)
os.makedirs(dirname)
@task
def move_in_tempdir(ctx):
recreate_dir(work_dirname)
with ctx.cd('main/doc'):
ctx.run('cp * {}/'.format(work_dirname))
@task(pre=[move_in_tempdir])
def create_listing(ctx):
with open('main/doc/_listing.adoc', 'w+') as listing:
listing.write(Template(filename='main/doc/listing.tpl').render(filenames=collectfilenames('main/src')))
@task(move_in_tempdir, create_listing)
def generate_pdf(ctx):
with ctx.cd(work_dirname):
ctx.run('asciidoctor-pdf doc.adoc')
@task(pre=[generate_pdf], default=True)
def copy_to_builddir(ctx):
dirname = 'build/doc'
recreate_dir(dirname)
with ctx.cd(dirname):
ctx.run('cp {}/*.pdf ./'.format(work_dirname))
===== FAQ =====
==== Comment récupérer le résultat d'une tâche ? ====
Si une tâche doit retourner un résultat pour la tâche suivante, comment le passer ?
Apparemment il n'est pas possible de transmettre un résultat par l'intermédiaire du contexte.
Le seul moyen est de peut-être utiliser un dictionnaire de stockage global.
storage = {}
@task
def do_something(ctx):
storage['foo'] = 'bar'
@task(pre=[do_something])
def use_something(ctx):
bar = storage['foo']
==== Comment répondre à une commande interactive ? ====
//Exemple tiré de la documentation officielle//
Supposons que nous ayons une commande qui demande des informations de façon interactive :
$ excitable-program
When you give the OK, I'm going to do the things. All of them!!
Are you ready? [Y/n] y
OK! I just did all sorts of neat stuff. You're welcome! Bye!
Pour cela il faut utiliser un //Responder// qui va surveiller la question et y répondre.
responder = Responder(pattern=r"Are you ready? \[Y/n\] ", response="y\n")
ctx.run("excitable-program", watchers=[responder])