Files
obitools4/blackboard/architechture/architecture-commande-obitools.md
Eric Coissac 00c8be6b48 docs: add architecture documentation for OBITools commands
Ajout d'une documentation détaillée sur l'architecture des commandes OBITools, incluant la structure modulaire, les patterns architecturaux et les bonnes pratiques pour la création de nouvelles commandes.
2026-02-07 12:26:35 +01:00

26 KiB

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/<nom_commande>/
  • L'exécutable dans cmd/obitools/<nom_commande>/

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/<commande>/

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) :

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) :

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) :

// 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 :

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/<commande>/main.go

Le fichier main.go de chaque commande est volontairement minimaliste et suit toujours le même pattern :

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

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 :

// 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/

mkdir -p pkg/obitools/macommande

Étape 2 : Créer options.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) :

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/

mkdir -p cmd/obitools/macommande

Créer main.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 :

// 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 :

// 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 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 :

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)

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 :

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 :

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)

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/<implementation>.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.