En Python, les pipelines Dagger utilisent l’async/await pour gérer les opérations asynchrones. La particularité de Dagger est sa lazy evaluation : le code que vous écrivez ne s’exécute pas immédiatement — il construit un graphe d’opérations qui sera exécuté quand vous le demanderez explicitement.
Prérequis : Avoir installé Dagger et créé un module. Si vous n’êtes pas familier avec async/await en Python, consultez la documentation officielle asyncio.
Le cycle de vie d’un pipeline Dagger
Section intitulée « Le cycle de vie d’un pipeline Dagger »Quand vous appelez des méthodes comme dag.container().from_(), vous ne lancez
pas de conteneurs — vous décrivez ce que vous voulez faire. L’exécution
réelle se produit seulement quand vous appelez une méthode “terminale” comme
stdout(), sync() ou export().
Lazy evaluation : pourquoi c’est important
Section intitulée « Lazy evaluation : pourquoi c’est important »La lazy evaluation offre plusieurs avantages :
- Optimisation automatique : Dagger peut réorganiser et paralléliser les opérations
- Cache intelligent : les étapes identiques sont exécutées une seule fois
- Composition : vous pouvez construire des pipelines complexes puis les exécuter
Quand l’exécution se déclenche-t-elle ?
Section intitulée « Quand l’exécution se déclenche-t-elle ? »L’exécution se déclenche quand vous appelez une méthode qui demande un résultat :
| Méthode | Ce qu’elle fait | Retour |
|---|---|---|
stdout() | Récupère la sortie standard | str |
stderr() | Récupère la sortie d’erreur | str |
sync() | Force l’exécution | l’objet lui-même |
export() | Exporte un fichier/répertoire | chemin |
contents() | Lit le contenu d’un fichier | str |
Fonctions synchrones vs asynchrones
Section intitulée « Fonctions synchrones vs asynchrones »Le SDK Python Dagger vous permet d’écrire deux types de fonctions.
Fonction synchrone (pas d’await)
Section intitulée « Fonction synchrone (pas d’await) »Une fonction synchrone retourne directement une valeur ou un objet Dagger sans attendre d’exécution :
@functiondef hello(self, name: str = "World") -> str: """Retourne un message — pas d'interaction avec Dagger.""" return f"Bonjour, {name} !"dagger call hello --name="DevOps"# Bonjour, DevOps !Vous pouvez aussi retourner un Container sans l’exécuter :
@functiondef build_container(self) -> dagger.Container: """Retourne un conteneur configuré (lazy - pas encore exécuté).""" return ( dag.container() .from_("python:3.12-slim") .with_exec(["python", "--version"]) )Ce code ne lance aucun conteneur. Il ne fait que décrire le pipeline.
Fonction asynchrone (avec await)
Section intitulée « Fonction asynchrone (avec await) »Une fonction asynchrone attend le résultat d’une opération qui force l’exécution :
@functionasync def get_python_version(self) -> str: """Récupère la version de Python — force l'exécution.""" result = await ( dag.container() .from_("python:3.12-slim") .with_exec(["python", "--version"]) .stdout() # <-- Force l'exécution ) return result.strip()dagger call get-python-version# Python 3.12.12Récupérer stdout et stderr
Section intitulée « Récupérer stdout et stderr »Dagger sépare proprement la sortie standard et la sortie d’erreur.
stdout() : la sortie standard
Section intitulée « stdout() : la sortie standard »@functionasync def get_output(self) -> str: """Récupère la sortie standard d'une commande.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["echo", "Hello from stdout"]) .stdout() )stderr() : la sortie d’erreur
Section intitulée « stderr() : la sortie d’erreur »@functionasync def get_error(self) -> str: """Récupère la sortie d'erreur d'une commande.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["sh", "-c", "echo 'Ceci va sur stderr' >&2"]) .stderr() )Les deux à la fois
Section intitulée « Les deux à la fois »Pour récupérer les deux sorties, stockez le conteneur dans une variable :
@functionasync def show_outputs(self) -> str: """Récupère stdout ET stderr séparément.""" container = ( dag.container() .from_("alpine:latest") .with_exec(["sh", "-c", "echo stdout && echo stderr >&2"]) )
stdout_content = await container.stdout() stderr_content = await container.stderr()
return f"STDOUT: {stdout_content.strip()} | STDERR: {stderr_content.strip()}"dagger call show-outputs# STDOUT: stdout | STDERR: stderrsync() : forcer l’exécution sans récupérer la sortie
Section intitulée « sync() : forcer l’exécution sans récupérer la sortie »Parfois, vous voulez vous assurer qu’une étape est exécutée sans récupérer
sa sortie. C’est le rôle de sync() :
@functionasync def prepare_environment(self) -> dagger.Container: """Prépare l'environnement et s'assure qu'il est construit.""" container = ( dag.container() .from_("python:3.12-slim") .with_exec(["pip", "install", "pytest"]) )
# sync() force l'exécution et retourne le même Container await container.sync()
return containerGestion des erreurs
Section intitulée « Gestion des erreurs »Quand une commande échoue dans un conteneur, Dagger lève une exception.
Vous pouvez la capturer avec try/except.
Capturer les erreurs d’exécution
Section intitulée « Capturer les erreurs d’exécution »import daggerfrom dagger import dag, function, object_type
@object_typeclass MonModule: @function async def demo_error_handling(self) -> str: """Capture les erreurs d'exécution.""" try: await ( dag.container() .from_("alpine:latest") .with_exec(["cat", "/fichier-inexistant"]) .stdout() ) return "Succès" except dagger.ExecError as e: return f"Erreur capturée : {e}"dagger call demo-error-handling# Erreur capturée : exit code: 1 [traceparent:...]Types d’erreurs Dagger
Section intitulée « Types d’erreurs Dagger »| Exception | Cause |
|---|---|
dagger.ExecError | Une commande with_exec() a échoué (code de sortie ≠ 0) |
dagger.QueryError | Erreur lors de l’appel à l’API Dagger |
Exception | Autres erreurs Python |
Pattern recommandé
Section intitulée « Pattern recommandé »@functionasync def safe_operation(self) -> str: """Pattern de gestion d'erreurs robuste.""" try: result = await ( dag.container() .from_("alpine:latest") .with_exec(["commande", "risquee"]) .stdout() ) return f"Succès : {result}" except dagger.ExecError as e: # Erreur de commande (code de sortie ≠ 0) return f"La commande a échoué : {e}" except dagger.QueryError as e: # Erreur d'API Dagger return f"Erreur Dagger : {e}" except Exception as e: # Autres erreurs return f"Erreur inattendue : {type(e).__name__}: {e}"Debug interactif
Section intitulée « Debug interactif »Dagger offre plusieurs outils pour debugger vos pipelines.
Terminal interactif
Section intitulée « Terminal interactif »Vous pouvez ouvrir un terminal dans n’importe quel conteneur :
dagger call build-container terminalCela ouvre un shell interactif dans le conteneur, où vous pouvez explorer l’état du système de fichiers, exécuter des commandes manuellement, etc.
Mode —interactive
Section intitulée « Mode —interactive »Si une commande échoue, vous pouvez relancer avec --interactive pour
obtenir un terminal au point d’échec :
dagger call --interactive ma-fonctionVerbosité des logs
Section intitulée « Verbosité des logs »Ajoutez des flags -v pour plus de détails :
dagger call -v ma-fonction # Garde les spans visiblesdagger call -vv ma-fonction # Révèle les spans internesdagger call -vvv ma-fonction # Révèle les spans < 100msdagger call --debug ma-fonction # Informations de debug complètesExemple complet commenté
Section intitulée « Exemple complet commenté »Cet exemple rassemble tous les concepts vus dans ce guide. Prenez le temps de le lire — chaque ligne applique une notion expliquée précédemment.
import daggerfrom dagger import dag, function, object_type
@object_typeclass MonPipeline: """Pipeline avec gestion async et erreurs."""
@function async def build_and_test(self, source: dagger.Directory) -> str: """Build et teste un projet Python.""" try: # ① LAZY : on décrit le pipeline, rien ne s'exécute encore container = ( dag.container() .from_("python:3.12-slim") .with_directory("/app", source) .with_workdir("/app") .with_exec(["pip", "install", "-e", "."]) )
# ② SYNC : on force l'exécution pour valider le build # Si pip échoue ici, on saute directement au except await container.sync()
# ③ STDOUT : on récupère la sortie des tests # C'est aussi un trigger, donc await obligatoire test_output = await ( container .with_exec(["pytest", "-v"]) .stdout() )
return f"Tests réussis :\n{test_output}"
except dagger.ExecError as e: # ④ ERREUR : une commande a échoué (pip install ou pytest) return f"Échec : {e}"Décryptage ligne par ligne
Section intitulée « Décryptage ligne par ligne »| Ligne | Concept | Ce qui se passe |
|---|---|---|
dag.container().from_()... | Lazy | On construit un graphe, aucun conteneur ne démarre |
await container.sync() | Trigger | L’Engine exécute tout jusqu’ici (pull image + pip install) |
await ... .stdout() | Async + Trigger | On attend le résultat de pytest |
except dagger.ExecError | Gestion erreurs | On capture les codes de sortie ≠ 0 |
Lancer l’exemple
Section intitulée « Lancer l’exemple »Pour tester ce pipeline sur un projet Python local :
# Depuis un dossier contenant pyproject.toml et des testsdagger call build-and-test --source=.Sortie en cas de succès :
Tests réussis :============================= test session starts ==============================collected 3 itemstests/test_main.py ... [100%]============================== 3 passed in 0.42s ===============================Sortie en cas d’échec :
Échec : exit code: 1 [traceparent:00-abc123...]Debugger pas à pas
Section intitulée « Debugger pas à pas »Scénario 1 : pip install échoue
# Lancer en mode interactif pour inspecter l'étatdagger call --interactive build-and-test --source=.Quand l’erreur se produit, un terminal s’ouvre dans le conteneur. Vous pouvez :
# Vérifier les fichiers présentsls -la /app
# Tester pip manuellementpip install -e . 2>&1
# Inspecter le pyproject.tomlcat /app/pyproject.tomlScénario 2 : pytest échoue
# Augmenter la verbosité pour voir les détailsdagger call -vv build-and-test --source=.Ou ouvrir un terminal sur le conteneur après le build :
# D'abord, créer une fonction qui retourne le conteneurdagger call build-container terminalPuis dans le terminal :
# Lancer pytest manuellement pour voir les erreurscd /app && pytest -v --tb=longScénario 3 : comprendre ce qui est caché
# Voir TOUT ce que Dagger fait en internedagger call --debug build-and-test --source=.Pourquoi sync() avant stdout() ?
Section intitulée « Pourquoi sync() avant stdout() ? »Dans l’exemple, on appelle sync() après pip install, puis stdout() après
pytest. Voici pourquoi :
| Sans sync() | Avec sync() |
|---|---|
| Si pip échoue, l’erreur est mélangée avec pytest | L’erreur pip est isolée, on sait exactement où ça casse |
| Impossible de savoir quelle étape a planté | Debug ciblé : build OK → tests KO |
| Moins d’overhead (1 seul trigger) | Légèrement plus lent (2 triggers) |
Règle : utilisez sync() entre les étapes logiques pour un debug précis.
Variante : récupérer stderr en cas d’échec
Section intitulée « Variante : récupérer stderr en cas d’échec »Pour avoir plus de détails sur l’erreur :
@functionasync def build_and_test_verbose(self, source: dagger.Directory) -> str: """Version avec diagnostic détaillé.""" container = ( dag.container() .from_("python:3.12-slim") .with_directory("/app", source) .with_workdir("/app") .with_exec(["pip", "install", "-e", "."]) )
try: await container.sync() except dagger.ExecError: # Récupérer stderr pour comprendre l'échec pip err = await container.stderr() return f"pip install a échoué :\n{err}"
test_container = container.with_exec(["pytest", "-v"])
try: output = await test_container.stdout() return f"Tests OK :\n{output}" except dagger.ExecError: err = await test_container.stderr() return f"pytest a échoué :\n{err}"À retenir
Section intitulée « À retenir »- Lazy evaluation :
dag.container()...ne s’exécute pas immédiatement - Triggers :
stdout(),stderr(),sync(),export(),contents()forcent l’exécution - Async : toute fonction qui appelle un trigger doit être
async defavecawait - Erreurs : utilisez
try/except dagger.ExecErrorpour capturer les échecs - Debug :
dagger call --interactiveouterminalpour explorer les conteneurs - Verbosité :
-v,-vv,-vvv,--debugpour plus d’informations
Prochaines étapes
Section intitulée « Prochaines étapes »Maintenant que vous maîtrisez le cycle async et la gestion des erreurs :
- Module 4 : Container API — Maîtriser
from_,with_exec, variables d’environnement (à venir) - Module 5 : Fichiers & workspace — Monter des répertoires, gérer les sorties (à venir)
Ressources complémentaires
Section intitulée « Ressources complémentaires »- Documentation officielle : docs.dagger.io
- Observability : docs.dagger.io/features/observability
- Python async/await : Documentation asyncio