diff --git a/blackboard/architechture/architecture-commande-obitools.md b/blackboard/architechture/architecture-commande-obitools.md new file mode 100644 index 0000000..2e515a0 --- /dev/null +++ b/blackboard/architechture/architecture-commande-obitools.md @@ -0,0 +1,735 @@ +# Architecture d'une commande OBITools + +## Vue d'ensemble + +Une commande OBITools suit une architecture modulaire et standardisée qui sépare clairement les responsabilités entre : +- Le package de la commande dans `pkg/obitools//` +- L'exécutable dans `cmd/obitools//` + +Cette architecture favorise la réutilisabilité du code, la testabilité et la cohérence entre les différentes commandes de la suite OBITools. + +## Structure du projet + +``` +obitools4/ +├── pkg/obitools/ +│ ├── obiconvert/ # Commande de conversion (base pour toutes) +│ │ ├── obiconvert.go # Fonctions vides (pas d'implémentation) +│ │ ├── options.go # Définition des options CLI +│ │ ├── sequence_reader.go # Lecture des séquences +│ │ └── sequence_writer.go # Écriture des séquences +│ ├── obiuniq/ # Commande de déréplication +│ │ ├── obiuniq.go # (fichier vide) +│ │ ├── options.go # Options spécifiques à obiuniq +│ │ └── unique.go # Implémentation du traitement +│ ├── obipairing/ # Assemblage de lectures paired-end +│ ├── obisummary/ # Résumé de fichiers de séquences +│ └── obimicrosat/ # Détection de microsatellites +└── cmd/obitools/ + ├── obiconvert/ + │ └── main.go # Point d'entrée de la commande + ├── obiuniq/ + │ └── main.go + ├── obipairing/ + │ └── main.go + ├── obisummary/ + │ └── main.go + └── obimicrosat/ + └── main.go +``` + +## Composants de l'architecture + +### 1. Package `pkg/obitools//` + +Chaque commande possède son propre package dans `pkg/obitools/` qui contient l'implémentation complète de la logique métier. Ce package est structuré en plusieurs fichiers : + +#### a) `options.go` - Gestion des options CLI + +Ce fichier définit : +- Les **variables globales** privées (préfixées par `_`) stockant les valeurs des options +- La fonction **`OptionSet()`** qui configure toutes les options pour la commande +- Les fonctions **`CLI*()`** qui retournent les valeurs des options (getters) +- Les fonctions **`Set*()`** qui permettent de définir les options programmatiquement (setters) + +**Exemple (obiuniq/options.go) :** + +```go +package obiuniq + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "github.com/DavidGamba/go-getoptions" +) + +// Variables globales privées pour stocker les options +var _StatsOn = make([]string, 0, 10) +var _Keys = make([]string, 0, 10) +var _InMemory = false +var _chunks = 100 + +// Configuration des options spécifiques à la commande +func UniqueOptionSet(options *getoptions.GetOpt) { + options.StringSliceVar(&_StatsOn, "merge", 1, 1, + options.Alias("m"), + options.ArgName("KEY"), + options.Description("Adds a merged attribute...")) + + options.BoolVar(&_InMemory, "in-memory", _InMemory, + options.Description("Use memory instead of disk...")) + + options.IntVar(&_chunks, "chunk-count", _chunks, + options.Description("In how many chunks...")) +} + +// OptionSet combine les options de base + les options spécifiques +func OptionSet(options *getoptions.GetOpt) { + obiconvert.OptionSet(false)(options) // Options de base + UniqueOptionSet(options) // Options spécifiques +} + +// Getters pour accéder aux valeurs des options +func CLIStatsOn() []string { + return _StatsOn +} + +func CLIUniqueInMemory() bool { + return _InMemory +} + +// Setters pour définir les options programmatiquement +func SetUniqueInMemory(inMemory bool) { + _InMemory = inMemory +} +``` + +**Convention de nommage :** +- Variables privées : `_NomOption` (underscore préfixe) +- Getters : `CLINomOption()` (préfixe CLI) +- Setters : `SetNomOption()` (préfixe Set) + +#### b) Fichier(s) d'implémentation + +Un ou plusieurs fichiers contenant la logique métier de la commande : + +**Exemple (obiuniq/unique.go) :** + +```go +package obiuniq + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obichunk" +) + +// Fonction CLI principale qui orchestre le traitement +func CLIUnique(sequences obiiter.IBioSequence) obiiter.IBioSequence { + // Récupération des options via les getters CLI*() + options := make([]obichunk.WithOption, 0, 30) + + options = append(options, + obichunk.OptionBatchCount(CLINumberOfChunks()), + ) + + if CLIUniqueInMemory() { + options = append(options, obichunk.OptionSortOnMemory()) + } else { + options = append(options, obichunk.OptionSortOnDisk()) + } + + // Appel de la fonction de traitement réelle + iUnique, err := obichunk.IUniqueSequence(sequences, options...) + + if err != nil { + log.Fatal(err) + } + + return iUnique +} +``` + +**Autres exemples d'implémentation :** + +- **obimicrosat/microsat.go** : Contient `MakeMicrosatWorker()` et `CLIAnnotateMicrosat()` +- **obisummary/obisummary.go** : Contient `ISummary()` et les structures de données + +#### c) Fichiers utilitaires (optionnel) + +Certaines commandes ont des fichiers additionnels pour des fonctionnalités spécifiques. + +**Exemple (obipairing/options.go) :** + +```go +// Fonction spéciale pour créer un itérateur de séquences pairées +func CLIPairedSequence() (obiiter.IBioSequence, error) { + forward, err := obiconvert.CLIReadBioSequences(_ForwardFile) + if err != nil { + return obiiter.NilIBioSequence, err + } + + reverse, err := obiconvert.CLIReadBioSequences(_ReverseFile) + if err != nil { + return obiiter.NilIBioSequence, err + } + + paired := forward.PairTo(reverse) + return paired, nil +} +``` + +### 2. Package `obiconvert` - La base commune + +Le package `obiconvert` est spécial car il fournit les fonctionnalités de base utilisées par toutes les autres commandes : + +#### Fonctionnalités fournies : + +1. **Lecture de séquences** (`sequence_reader.go`) + - `CLIReadBioSequences()` : lecture depuis fichiers ou stdin + - Support de multiples formats (FASTA, FASTQ, EMBL, GenBank, etc.) + - Gestion des fichiers multiples + - Barre de progression optionnelle + +2. **Écriture de séquences** (`sequence_writer.go`) + - `CLIWriteBioSequences()` : écriture vers fichiers ou stdout + - Support de multiples formats + - Gestion des lectures pairées + - Compression optionnelle + +3. **Options communes** (`options.go`) + - Options d'entrée (format, skip, etc.) + - Options de sortie (format, fichier, compression) + - Options de mode (barre de progression, etc.) + +#### Utilisation par les autres commandes : + +Toutes les commandes incluent les options de `obiconvert` via : + +```go +func OptionSet(options *getoptions.GetOpt) { + obiconvert.OptionSet(false)(options) // false = pas de fichiers pairés + MaCommandeOptionSet(options) // Options spécifiques +} +``` + +### 3. Exécutable `cmd/obitools//main.go` + +Le fichier `main.go` de chaque commande est volontairement **minimaliste** et suit toujours le même pattern : + +```go +package main + +import ( + "os" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/macommande" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +func main() { + // 1. Configuration optionnelle de paramètres par défaut + obidefault.SetBatchSize(10) + + // 2. Génération du parser d'options + optionParser := obioptions.GenerateOptionParser( + "macommande", // Nom de la commande + "description de la commande", // Description + macommande.OptionSet) // Fonction de configuration des options + + // 3. Parsing des arguments + _, args := optionParser(os.Args) + + // 4. Lecture des séquences d'entrée + sequences, err := obiconvert.CLIReadBioSequences(args...) + obiconvert.OpenSequenceDataErrorMessage(args, err) + + // 5. Traitement spécifique de la commande + resultat := macommande.CLITraitement(sequences) + + // 6. Écriture des résultats + obiconvert.CLIWriteBioSequences(resultat, true) + + // 7. Attente de la fin du pipeline + obiutils.WaitForLastPipe() +} +``` + +## Patterns architecturaux + +### Pattern 1 : Pipeline de traitement de séquences + +La plupart des commandes suivent ce pattern : + +``` +Lecture → Traitement → Écriture +``` + +**Exemples :** +- **obiconvert** : Lecture → Écriture (conversion de format) +- **obiuniq** : Lecture → Déréplication → Écriture +- **obimicrosat** : Lecture → Annotation → Filtrage → Écriture + +### Pattern 2 : Traitement avec entrées multiples + +Certaines commandes acceptent plusieurs fichiers d'entrée : + +**obipairing** : +``` +Lecture Forward + Lecture Reverse → Pairing → Assemblage → Écriture +``` + +### Pattern 3 : Traitement sans écriture de séquences + +**obisummary** : produit un résumé JSON/YAML au lieu de séquences + +```go +func main() { + // ... parsing options et lecture ... + + summary := obisummary.ISummary(fs, obisummary.CLIMapSummary()) + + // Formatage et affichage direct + if obisummary.CLIOutFormat() == "json" { + output, _ := json.MarshalIndent(summary, "", " ") + fmt.Print(string(output)) + } else { + output, _ := yaml.Marshal(summary) + fmt.Print(string(output)) + } +} +``` + +### Pattern 4 : Utilisation de Workers + +Les commandes qui transforment des séquences utilisent souvent le pattern Worker : + +```go +// Création d'un worker +worker := MakeMicrosatWorker( + CLIMinUnitLength(), + CLIMaxUnitLength(), + // ... autres paramètres +) + +// Application du worker sur l'itérateur +newIter = iterator.MakeIWorker( + worker, + false, // merge results + obidefault.ParallelWorkers() // parallélisation +) +``` + +## Étapes d'implémentation d'une nouvelle commande + +### Étape 1 : Créer le package dans `pkg/obitools/` + +```bash +mkdir -p pkg/obitools/macommande +``` + +### Étape 2 : Créer `options.go` + +```go +package macommande + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "github.com/DavidGamba/go-getoptions" +) + +// Variables privées pour les options +var _MonOption = "valeur_par_defaut" + +// Configuration des options spécifiques +func MaCommandeOptionSet(options *getoptions.GetOpt) { + options.StringVar(&_MonOption, "mon-option", _MonOption, + options.Alias("o"), + options.Description("Description de l'option")) +} + +// OptionSet combine options de base + spécifiques +func OptionSet(options *getoptions.GetOpt) { + obiconvert.OptionSet(false)(options) // false si pas de fichiers pairés + MaCommandeOptionSet(options) +} + +// Getters +func CLIMonOption() string { + return _MonOption +} + +// Setters +func SetMonOption(value string) { + _MonOption = value +} +``` + +### Étape 3 : Créer le fichier d'implémentation + +Créer `macommande.go` (ou un nom plus descriptif) : + +```go +package macommande + +import ( + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" +) + +// Fonction de traitement principale +func CLIMaCommande(sequences obiiter.IBioSequence) obiiter.IBioSequence { + // Récupération des options + option := CLIMonOption() + + // Implémentation du traitement + // ... + + return resultat +} +``` + +### Étape 4 : Créer l'exécutable dans `cmd/obitools/` + +```bash +mkdir -p cmd/obitools/macommande +``` + +Créer `main.go` : + +```go +package main + +import ( + "os" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obioptions" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/obiconvert" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitools/macommande" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" +) + +func main() { + // Parser d'options + optionParser := obioptions.GenerateOptionParser( + "macommande", + "Description courte de ma commande", + macommande.OptionSet) + + _, args := optionParser(os.Args) + + // Lecture + sequences, err := obiconvert.CLIReadBioSequences(args...) + obiconvert.OpenSequenceDataErrorMessage(args, err) + + // Traitement + resultat := macommande.CLIMaCommande(sequences) + + // Écriture + obiconvert.CLIWriteBioSequences(resultat, true) + + // Attente + obiutils.WaitForLastPipe() +} +``` + +### Étape 5 : Configurations optionnelles + +Dans `main.go`, avant le parsing des options, on peut configurer : + +```go +// Taille des batchs de séquences +obidefault.SetBatchSize(10) + +// Nombre de workers en lecture (strict) +obidefault.SetStrictReadWorker(2) + +// Nombre de workers en écriture +obidefault.SetStrictWriteWorker(2) + +// Désactiver la lecture des qualités +obidefault.SetReadQualities(false) +``` + +### Étape 6 : Gestion des erreurs + +Utiliser les fonctions utilitaires pour les messages d'erreur cohérents : + +```go +// Pour les erreurs d'ouverture de fichiers +obiconvert.OpenSequenceDataErrorMessage(args, err) + +// Pour les erreurs générales +if err != nil { + log.Errorf("Message d'erreur: %v", err) + os.Exit(1) +} +``` + +### Étape 7 : Tests et debugging (optionnel) + +Des commentaires dans le code montrent comment activer le profiling : + +```go +// go tool pprof -http=":8000" ./macommande ./cpu.pprof +// f, err := os.Create("cpu.pprof") +// if err != nil { +// log.Fatal(err) +// } +// pprof.StartCPUProfile(f) +// defer pprof.StopCPUProfile() + +// go tool trace cpu.trace +// ftrace, err := os.Create("cpu.trace") +// if err != nil { +// log.Fatal(err) +// } +// trace.Start(ftrace) +// defer trace.Stop() +``` + +## Bonnes pratiques observées + +### 1. Séparation des responsabilités + +- **`main.go`** : orchestration minimale +- **`options.go`** : définition et gestion des options +- **Fichiers d'implémentation** : logique métier + +### 2. Convention de nommage cohérente + +- Variables d'options : `_NomOption` +- Getters CLI : `CLINomOption()` +- Setters : `SetNomOption()` +- Fonctions de traitement CLI : `CLITraitement()` + +### 3. Réutilisation du code + +- Toutes les commandes réutilisent `obiconvert` pour l'I/O +- Les options communes sont partagées +- Les fonctions utilitaires sont centralisées + +### 4. Configuration par défaut + +Les valeurs par défaut sont : +- Définies lors de l'initialisation des variables +- Modifiables via les options CLI +- Modifiables programmatiquement via les setters + +### 5. Gestion des formats + +Support automatique de multiples formats : +- FASTA / FASTQ (avec compression gzip) +- EMBL / GenBank +- ecoPCR +- CSV +- JSON (avec différents formats d'en-têtes) + +### 6. Parallélisation + +Les commandes utilisent les workers parallèles via : +- `obidefault.ParallelWorkers()` +- `obidefault.SetStrictReadWorker(n)` +- `obidefault.SetStrictWriteWorker(n)` + +### 7. Logging cohérent + +Utilisation de `logrus` pour tous les logs : +```go +log.Printf("Message informatif") +log.Errorf("Message d'erreur: %v", err) +log.Fatal(err) // Arrêt du programme +``` + +## Dépendances principales + +### Packages internes OBITools + +- `pkg/obidefault` : valeurs par défaut et configuration globale +- `pkg/obioptions` : génération du parser d'options +- `pkg/obiiter` : itérateurs de séquences biologiques +- `pkg/obiseq` : structures et fonctions pour séquences biologiques +- `pkg/obiformats` : lecture/écriture de différents formats +- `pkg/obiutils` : fonctions utilitaires diverses +- `pkg/obichunk` : traitement par chunks (pour dereplication, etc.) + +### Packages externes + +- `github.com/DavidGamba/go-getoptions` : parsing des options CLI +- `github.com/sirupsen/logrus` : logging structuré +- `gopkg.in/yaml.v3` : encodage/décodage YAML +- `github.com/dlclark/regexp2` : expressions régulières avancées + +## Cas spéciaux + +### Commande avec fichiers pairés (obipairing) + +```go +func OptionSet(options *getoptions.GetOpt) { + obiconvert.OutputOptionSet(options) + obiconvert.InputOptionSet(options) + PairingOptionSet(options) // Options spécifiques au pairing +} + +func CLIPairedSequence() (obiiter.IBioSequence, error) { + forward, err := obiconvert.CLIReadBioSequences(_ForwardFile) + // ... + reverse, err := obiconvert.CLIReadBioSequences(_ReverseFile) + // ... + paired := forward.PairTo(reverse) + return paired, nil +} +``` + +Dans `main.go` : +```go +pairs, err := obipairing.CLIPairedSequence() // Lecture spéciale +if err != nil { + log.Errorf("Cannot open file (%v)", err) + os.Exit(1) +} + +paired := obipairing.IAssemblePESequencesBatch( + pairs, + obipairing.CLIGapPenality(), + // ... autres paramètres +) +``` + +### Commande sans sortie de séquences (obisummary) + +Au lieu de `obiconvert.CLIWriteBioSequences()`, affichage direct : + +```go +summary := obisummary.ISummary(fs, obisummary.CLIMapSummary()) + +if obisummary.CLIOutFormat() == "json" { + output, _ := json.MarshalIndent(summary, "", " ") + fmt.Print(string(output)) +} else { + output, _ := yaml.Marshal(summary) + fmt.Print(string(output)) +} +fmt.Printf("\n") +``` + +### Commande avec Workers personnalisés (obimicrosat) + +```go +func CLIAnnotateMicrosat(iterator obiiter.IBioSequence) obiiter.IBioSequence { + // Création du worker + worker := MakeMicrosatWorker( + CLIMinUnitLength(), + CLIMaxUnitLength(), + CLIMinUnitCount(), + CLIMinLength(), + CLIMinFlankLength(), + CLIReoriented(), + ) + + // Application du worker + newIter := iterator.MakeIWorker( + worker, + false, // pas de merge + obidefault.ParallelWorkers(), // parallélisation + ) + + return newIter.FilterEmpty() // Filtrage des résultats vides +} +``` + +## Diagramme de flux d'exécution + +``` +┌─────────────────────────────────────────────────────────────┐ +│ cmd/obitools/macommande/main.go │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 1. Génération du parser d'options │ +│ obioptions.GenerateOptionParser( │ +│ "macommande", │ +│ "description", │ +│ macommande.OptionSet) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ pkg/obitools/macommande/options.go │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ func OptionSet(options *getoptions.GetOpt) │ │ +│ │ obiconvert.OptionSet(false)(options) ───────────┐ │ │ +│ │ MaCommandeOptionSet(options) │ │ │ +│ └───────────────────────────────────────────────────┼─┘ │ +└────────────────────────────────────────────────────────┼─────┘ + │ │ + │ │ + ┌─────────────┘ │ + │ │ + ▼ ▼ +┌─────────────────────────────────┐ ┌───────────────────────────────┐ +│ 2. Parsing des arguments │ │ pkg/obitools/obiconvert/ │ +│ _, args := optionParser(...) │ │ options.go │ +└─────────────────────────────────┘ │ - InputOptionSet() │ + │ │ - OutputOptionSet() │ + ▼ │ - PairedFilesOptionSet() │ +┌─────────────────────────────────┐ └───────────────────────────────┘ +│ 3. Lecture des séquences │ +│ CLIReadBioSequences(args) │ +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ pkg/obitools/obiconvert/sequence_reader.go │ +│ - ExpandListOfFiles() │ +│ - ReadSequencesFromFile() / ReadSequencesFromStdin() │ +│ - Support: FASTA, FASTQ, EMBL, GenBank, ecoPCR, CSV │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ obiiter.IBioSequence +┌─────────────────────────────────────────────────────────────┐ +│ 4. Traitement spécifique │ +│ macommande.CLITraitement(sequences) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ pkg/obitools/macommande/.go │ +│ - Récupération des options via CLI*() getters │ +│ - Application de la logique métier │ +│ - Retour d'un nouvel iterator │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ obiiter.IBioSequence +┌─────────────────────────────────────────────────────────────┐ +│ 5. Écriture des résultats │ +│ CLIWriteBioSequences(resultat, true) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ pkg/obitools/obiconvert/sequence_writer.go │ +│ - WriteSequencesToFile() / WriteSequencesToStdout() │ +│ - Support: FASTA, FASTQ, JSON │ +│ - Gestion des lectures pairées │ +│ - Compression optionnelle │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 6. Attente de fin du pipeline │ +│ obiutils.WaitForLastPipe() │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Conclusion + +L'architecture des commandes OBITools est conçue pour : + +1. **Maximiser la réutilisation** : `obiconvert` fournit les fonctionnalités communes +2. **Simplifier l'ajout de nouvelles commandes** : pattern standardisé et minimaliste +3. **Faciliter la maintenance** : séparation claire des responsabilités +4. **Garantir la cohérence** : conventions de nommage et structure uniforme +5. **Optimiser les performances** : parallélisation intégrée et traitement par batch + +Cette architecture modulaire permet de créer rapidement de nouvelles commandes tout en maintenant une qualité et une cohérence élevées dans toute la suite OBITools.